diff --git a/android/src/main/java/com/brentvatne/common/api/ControlsConfig.kt b/android/src/main/java/com/brentvatne/common/api/ControlsConfig.kt index 52736db2..a50482c6 100644 --- a/android/src/main/java/com/brentvatne/common/api/ControlsConfig.kt +++ b/android/src/main/java/com/brentvatne/common/api/ControlsConfig.kt @@ -7,6 +7,8 @@ class ControlsConfig { var hideSeekBar: Boolean = false var seekIncrementMS: Int = 10000 var hideDuration: Boolean = false + var hideNavigationBarOnFullScreenMode: Boolean = true + var hideNotificationBarOnFullScreenMode: Boolean = true companion object { @JvmStatic @@ -17,8 +19,9 @@ class ControlsConfig { config.hideSeekBar = ReactBridgeUtils.safeGetBool(src, "hideSeekBar", false) config.seekIncrementMS = ReactBridgeUtils.safeGetInt(src, "seekIncrementMS", 10000) config.hideDuration = ReactBridgeUtils.safeGetBool(src, "hideDuration", false) + config.hideNavigationBarOnFullScreenMode = ReactBridgeUtils.safeGetBool(src, "hideNavigationBarOnFullScreenMode", true) + config.hideNotificationBarOnFullScreenMode = ReactBridgeUtils.safeGetBool(src, "hideNotificationBarOnFullScreenMode", true) } - return config } } diff --git a/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt b/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt index 00f93975..1b9b1ce3 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt +++ b/android/src/main/java/com/brentvatne/exoplayer/FullScreenPlayerView.kt @@ -6,11 +6,16 @@ import android.content.Context import android.os.Handler import android.os.Looper import android.view.ViewGroup +import android.view.Window import android.view.WindowManager import android.widget.FrameLayout import android.widget.ImageButton import androidx.activity.OnBackPressedCallback +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import androidx.media3.ui.LegacyPlayerControlView +import com.brentvatne.common.api.ControlsConfig import com.brentvatne.common.toolbox.DebugLog import java.lang.ref.WeakReference @@ -20,14 +25,22 @@ class FullScreenPlayerView( private val exoPlayerView: ExoPlayerView, private val reactExoplayerView: ReactExoplayerView, private val playerControlView: LegacyPlayerControlView?, - private val onBackPressedCallback: OnBackPressedCallback -) : Dialog(context, android.R.style.Theme_Black_NoTitleBar_Fullscreen) { + private val onBackPressedCallback: OnBackPressedCallback, + private val controlsConfig: ControlsConfig +) : Dialog(context, android.R.style.Theme_Black_NoTitleBar) { private var parent: ViewGroup? = null private val containerView = FrameLayout(context) private val mKeepScreenOnHandler = Handler(Looper.getMainLooper()) private val mKeepScreenOnUpdater = KeepScreenOnUpdater(this) + // As this view is fullscreen we need to save initial state and restore it afterward + // Following variables save UI state when open the view + // restoreUIState, will reapply these values + private var initialSystemBarsBehavior: Int? = null + private var initialNavigationBarIsVisible: Boolean? = null + private var initialNotificationBarIsVisible: Boolean? = null + private class KeepScreenOnUpdater(fullScreenPlayerView: FullScreenPlayerView) : Runnable { private val mFullscreenPlayer = WeakReference(fullScreenPlayerView) @@ -59,6 +72,15 @@ class FullScreenPlayerView( init { setContentView(containerView, generateDefaultLayoutParams()) + + window?.let { + val inset = WindowInsetsControllerCompat(it, it.decorView) + initialSystemBarsBehavior = inset.systemBarsBehavior + initialNavigationBarIsVisible = ViewCompat.getRootWindowInsets(it.decorView) + ?.isVisible(WindowInsetsCompat.Type.navigationBars()) == true + initialNotificationBarIsVisible = ViewCompat.getRootWindowInsets(it.decorView) + ?.isVisible(WindowInsetsCompat.Type.statusBars()) == true + } } override fun onBackPressed() { super.onBackPressed() @@ -75,6 +97,7 @@ class FullScreenPlayerView( parent?.removeView(it) containerView.addView(it, generateDefaultLayoutParams()) } + updateNavigationBarVisibility() } override fun onStop() { @@ -89,6 +112,19 @@ class FullScreenPlayerView( } parent?.requestLayout() parent = null + restoreSystemUI() + } + + // restore system UI state + private fun restoreSystemUI() { + window?.let { + updateNavigationBarVisibility( + it, + initialNavigationBarIsVisible, + initialNotificationBarIsVisible, + initialSystemBarsBehavior + ) + } } private fun getFullscreenIconResource(isFullscreen: Boolean): Int = @@ -127,4 +163,61 @@ class FullScreenPlayerView( layoutParams.setMargins(0, 0, 0, 0) return layoutParams } + + private fun updateBarVisibility( + inset: WindowInsetsControllerCompat, + type: Int, + shouldHide: Boolean?, + initialVisibility: Boolean?, + systemBarsBehavior: Int? = null + ) { + shouldHide?.takeIf { it != initialVisibility }?.let { + if (it) { + inset.hide(type) + systemBarsBehavior?.let { behavior -> inset.systemBarsBehavior = behavior } + } else { + inset.show(type) + } + } + } + + // Move the UI to fullscreen. + // if you change this code, remember to check that the UI is well restored in restoreUIState + private fun updateNavigationBarVisibility( + window: Window, + hideNavigationBarOnFullScreenMode: Boolean?, + hideNotificationBarOnFullScreenMode: Boolean?, + systemBarsBehavior: Int? + ) { + // Configure the behavior of the hidden system bars. + val inset = WindowInsetsControllerCompat(window, window.decorView) + + // Update navigation bar visibility and apply systemBarsBehavior if hiding + updateBarVisibility( + inset, + WindowInsetsCompat.Type.navigationBars(), + hideNavigationBarOnFullScreenMode, + initialNavigationBarIsVisible, + systemBarsBehavior + ) + + // Update notification bar visibility (no need for systemBarsBehavior here) + updateBarVisibility( + inset, + WindowInsetsCompat.Type.statusBars(), + hideNotificationBarOnFullScreenMode, + initialNotificationBarIsVisible + ) + } + + private fun updateNavigationBarVisibility() { + window?.let { + updateNavigationBarVisibility( + it, + controlsConfig.hideNavigationBarOnFullScreenMode, + controlsConfig.hideNotificationBarOnFullScreenMode, + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + ) + } + } } diff --git a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java index c3933ef8..01d6a223 100644 --- a/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java +++ b/android/src/main/java/com/brentvatne/exoplayer/ReactExoplayerView.java @@ -427,15 +427,6 @@ public class ReactExoplayerView extends FrameLayout implements }); } - if (fullScreenPlayerView == null) { - fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, playerControlView, new OnBackPressedCallback(true) { - @Override - public void handleOnBackPressed() { - setFullscreen(false); - } - }); - } - // Setting the player for the playerControlView playerControlView.setPlayer(player); playPauseControlContainer = playerControlView.findViewById(R.id.exo_play_pause_container); @@ -2261,6 +2252,12 @@ public class ReactExoplayerView extends FrameLayout implements } if (isFullscreen) { + fullScreenPlayerView = new FullScreenPlayerView(getContext(), exoPlayerView, this, playerControlView, new OnBackPressedCallback(true) { + @Override + public void handleOnBackPressed() { + setFullscreen(false); + } + }, controlsConfig); eventEmitter.onVideoFullscreenPlayerWillPresent.invoke(); if (fullScreenPlayerView != null) { fullScreenPlayerView.show(); @@ -2383,4 +2380,4 @@ public class ReactExoplayerView extends FrameLayout implements controlsConfig = controlsStyles; refreshProgressBarVisibility(); } -} +} \ No newline at end of file diff --git a/docs/pages/component/props.mdx b/docs/pages/component/props.mdx index 47971ba8..655161cd 100644 --- a/docs/pages/component/props.mdx +++ b/docs/pages/component/props.mdx @@ -144,11 +144,13 @@ If needed, you can also add your controls or use a package like [react-native-vi Adjust the control styles. This prop is need only if `controls={true}` and is an object. See the list of prop supported below. -| Property | Type | Description | -|-----------------|---------|-----------------------------------------------------------------------------------------| -| hideSeekBar | boolean | The default value is `false`, allowing you to hide the seek bar for live broadcasts. | -| hideDuration | boolean | The default value is `false`, allowing you to hide the duration. | -| seekIncrementMS | number | The default value is `10000`. You can change the value to increment forward and rewind. | +| Property | Type | Description | +|-----------------------------------|---------|--------------------------------------------------------------------------------------------| +| hideSeekBar | boolean | The default value is `false`, allowing you to hide the seek bar for live broadcasts. | +| hideDuration | boolean | The default value is `false`, allowing you to hide the duration. | +| seekIncrementMS | number | The default value is `10000`. You can change the value to increment forward and rewind. | +| hideNavigationBarOnFullScreenMode | boolean | The default value is `true`, allowing you to hide the navigation bar on full-screen mode. | +| hideNotificationBarOnFullScreenMode | boolean | The default value is `true`, allowing you to hide the notification bar on full-screen mode. | Example with default values: @@ -157,6 +159,8 @@ controlsStyles={{ hideSeekBar: false, hideDuration: false, seekIncrementMS: 10000, + hideNavigationBarOnFullScreenMode: true, + hideNotificationBarOnFullScreenMode: true, }} ``` @@ -1089,4 +1093,4 @@ Example: }} // or other video props /> -``` \ No newline at end of file +``` diff --git a/examples/basic/package.json b/examples/basic/package.json index 5e0665bc..a0fcc356 100644 --- a/examples/basic/package.json +++ b/examples/basic/package.json @@ -17,6 +17,7 @@ "expo": "^51.0.31", "expo-asset": "~10.0.10", "expo-image": "^1.12.15", + "expo-navigation-bar": "~3.0.7", "react": "18.2.0", "react-native": "0.74.5", "react-native-windows": "0.74.19" diff --git a/examples/basic/src/VideoPlayer.tsx b/examples/basic/src/VideoPlayer.tsx index 83e66db7..53e36e2d 100644 --- a/examples/basic/src/VideoPlayer.tsx +++ b/examples/basic/src/VideoPlayer.tsx @@ -1,8 +1,8 @@ 'use strict'; -import React, {type FC, useCallback, useRef, useState} from 'react'; +import React, {type FC, useCallback, useRef, useState, useEffect} from 'react'; -import {Platform, TouchableOpacity, View} from 'react-native'; +import {Platform, TouchableOpacity, View, StatusBar} from 'react-native'; import Video, { VideoRef, @@ -36,6 +36,7 @@ import styles from './styles'; import {type AdditionalSourceInfo} from './types'; import {bufferConfig, srcList, textTracksSelectionBy} from './constants'; import {Overlay, toast, VideoLoader} from './components'; +import * as NavigationBar from 'expo-navigation-bar'; type Props = NonNullable; @@ -104,6 +105,10 @@ const VideoPlayer: FC = ({}) => { goToChannel((srcListId + srcList.length - 1) % srcList.length); }, [goToChannel, srcListId]); + useEffect(() => { + NavigationBar.setVisibilityAsync('visible'); + }, []); + const onAudioTracks = (data: OnAudioTracksData) => { const selectedTrack = data.audioTracks?.find((x: AudioTrack) => { return x.selected; @@ -226,6 +231,8 @@ const VideoPlayer: FC = ({}) => { return ( +