From b8524ccab6035177c7e5969de65cb610bff306db Mon Sep 17 00:00:00 2001 From: ThibaultBee <37510686+ThibaultBee@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:57:57 +0100 Subject: [PATCH] feat(*): improve full screen usage for view based player --- README.md | 24 +++++-- .../video/api/player/example/MainActivity.kt | 69 +++++-------------- player/build.gradle | 2 + .../api/player/ApiVideoPlayerController.kt | 16 ++++- .../api/player/extensions/WindowExtensions.kt | 20 ++++++ .../ApiVideoPlayerFullScreenController.kt | 69 +++++++++++++++++++ .../api/player/views/ApiVideoExoPlayerView.kt | 28 +++++--- .../player/views/FullScreenDialogFragment.kt | 54 +++++++++++++++ .../src/main/res/layout/exo_player_layout.xml | 4 +- .../src/main/res/layout/fullscreen_layout.xml | 6 ++ player/src/main/res/values/styles.xml | 11 +++ 11 files changed, 231 insertions(+), 72 deletions(-) create mode 100644 player/src/main/java/video/api/player/extensions/WindowExtensions.kt create mode 100644 player/src/main/java/video/api/player/models/ApiVideoPlayerFullScreenController.kt create mode 100644 player/src/main/java/video/api/player/views/FullScreenDialogFragment.kt create mode 100644 player/src/main/res/layout/fullscreen_layout.xml diff --git a/README.md b/README.md index a80191d..6043eca 100644 --- a/README.md +++ b/README.md @@ -72,11 +72,11 @@ analytics of [your viewers usage](https://api.video/product/video-analytics/). ```xml - ``` @@ -101,7 +101,7 @@ val playerControllerListener = object : ApiVideoPlayerController.Listener { ```kotlin val playerView = findViewById(R.id.playerView) -val player = ApiVideoPlayerController( +val playerController = ApiVideoPlayerController( applicationContext, VideoOptions( "YOUR_VIDEO_ID", @@ -114,8 +114,20 @@ val player = ApiVideoPlayerController( 4. Fullscreen video -If you requires a fullscreen video. You will have to implement -the `ApiVideoPlayerController.ViewListener` interface. +If you require a fullscreen video. You will have to implement +the `ApiVideoExoPlayerView.FullScreenListener` interface. +To help you, you can use the `ApiVideoPlayerFullScreenController` that will handle the fullscreen. +As it implements the `ApiVideoExoPlayerView.FullScreenListener` interface, you can pass it to +your `ApiVideoExoPlayerView` instance. + +```kotlin +playerView.fullScreenListener = ApiVideoExoPlayerView( + supportFragmentManager, + playerView, + playerController +) +``` + Check out for the implementation in the [Sample application](#sample-application). ### Supported player views diff --git a/examples/view/src/main/java/video/api/player/example/MainActivity.kt b/examples/view/src/main/java/video/api/player/example/MainActivity.kt index 01313c0..cbc10e6 100644 --- a/examples/view/src/main/java/video/api/player/example/MainActivity.kt +++ b/examples/view/src/main/java/video/api/player/example/MainActivity.kt @@ -7,17 +7,14 @@ import android.os.Bundle import android.util.Log import android.util.Size import android.view.View -import android.view.ViewGroup import android.widget.PopupMenu import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.WindowInsetsControllerCompat import androidx.preference.PreferenceManager import com.android.volley.ClientError import com.google.android.material.snackbar.Snackbar import video.api.player.ApiVideoPlayerController import video.api.player.example.databinding.ActivityMainBinding +import video.api.player.models.ApiVideoPlayerFullScreenController import video.api.player.models.VideoOptions import video.api.player.models.VideoType import video.api.player.views.ApiVideoExoPlayerView @@ -51,33 +48,6 @@ class MainActivity : AppCompatActivity() { } } - private val fullScreenListener = object : ApiVideoExoPlayerView.FullScreenListener { - override fun onFullScreenModeChanged(isFullScreen: Boolean) { - /** - * For fullscreen video, hides every views and forces orientation in landscape. - */ - if (isFullScreen) { - supportActionBar?.hide() - hideSystemUI() - binding.fab.hide() - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE - binding.playerView.layoutParams.apply { - width = ViewGroup.LayoutParams.MATCH_PARENT - height = ViewGroup.LayoutParams.MATCH_PARENT - } - } else { - supportActionBar?.show() - showSystemUI() - binding.fab.show() - requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR - binding.playerView.layoutParams.apply { - width = ViewGroup.LayoutParams.WRAP_CONTENT - height = ViewGroup.LayoutParams.WRAP_CONTENT - } - } - } - } - private val playerControllerListener = object : ApiVideoPlayerController.Listener { override fun onError(error: Exception) { val message = when { @@ -125,8 +95,23 @@ class MainActivity : AppCompatActivity() { } } + private val fullScreenController: ApiVideoPlayerFullScreenController by lazy { + ApiVideoPlayerFullScreenController( + supportFragmentManager, + binding.playerView, + playerController, + object : ApiVideoExoPlayerView.FullScreenListener { + override fun onFullScreenModeChanged(isFullScreen: Boolean) { + requestedOrientation = if (isFullScreen) { + ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE + } else { + ActivityInfo.SCREEN_ORIENTATION_SENSOR + } + } + } + ) + } private val playerController: ApiVideoPlayerController by lazy { - binding.playerView.fullScreenListener = fullScreenListener ApiVideoPlayerController( applicationContext, null, @@ -140,23 +125,6 @@ class MainActivity : AppCompatActivity() { Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show() } - private fun hideSystemUI() { - WindowCompat.setDecorFitsSystemWindows(window, false) - WindowInsetsControllerCompat(window, window.decorView).let { controller -> - controller.hide(WindowInsetsCompat.Type.systemBars()) - controller.systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } - } - - private fun showSystemUI() { - WindowCompat.setDecorFitsSystemWindows(window, true) - WindowInsetsControllerCompat( - window, - window.decorView - ).show(WindowInsetsCompat.Type.systemBars()) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -187,8 +155,9 @@ class MainActivity : AppCompatActivity() { } binding.unmute.setOnClickListener { playerController.isMuted = false } + binding.playerView.fullScreenListener = fullScreenController binding.showFullScreenButton.setOnClickListener { - binding.playerView.fullScreenListener = fullScreenListener + binding.playerView.fullScreenListener = fullScreenController } binding.hideFullScreenButton.setOnClickListener { binding.playerView.fullScreenListener = null diff --git a/player/build.gradle b/player/build.gradle index f6a842d..838cfe5 100644 --- a/player/build.gradle +++ b/player/build.gradle @@ -24,6 +24,8 @@ dependencies { implementation "androidx.media3:media3-exoplayer:${exoPlayerVersion}" implementation "androidx.media3:media3-exoplayer-hls:${exoPlayerVersion}" implementation "androidx.media:media:1.6.0" + implementation "androidx.fragment:fragment-ktx:1.6.2" + implementation "com.google.android.material:material:1.10.0" testImplementation 'org.robolectric:robolectric:4.10.3' testImplementation 'org.robolectric:shadows-httpclient:4.5.1' diff --git a/player/src/main/java/video/api/player/ApiVideoPlayerController.kt b/player/src/main/java/video/api/player/ApiVideoPlayerController.kt index 4fd1fe9..38f985a 100644 --- a/player/src/main/java/video/api/player/ApiVideoPlayerController.kt +++ b/player/src/main/java/video/api/player/ApiVideoPlayerController.kt @@ -1,5 +1,6 @@ package video.api.player +import android.annotation.SuppressLint import android.content.Context import android.os.Handler import android.os.Looper @@ -597,9 +598,7 @@ constructor( * * @param view the player view. An [ApiVideoExoPlayerView] for example. */ - fun setPlayerView(view: IExoPlayerBasedPlayerView) { - view.playerView.player = exoplayer - } + fun setPlayerView(view: IExoPlayerBasedPlayerView) = setPlayerView(view.playerView) /** * Sets the player view @@ -637,6 +636,17 @@ constructor( exoplayer.setVideoSurface(surface) } + @SuppressLint("UnsafeOptInUsageError") + fun switchTargetView(oldPlayerView: PlayerView, newPlayerView: PlayerView) { + PlayerView.switchTargetView(exoplayer, oldPlayerView, newPlayerView) + } + + @SuppressLint("UnsafeOptInUsageError") + fun switchTargetView( + oldPlayerView: IExoPlayerBasedPlayerView, + newPlayerView: IExoPlayerBasedPlayerView + ) = switchTargetView(oldPlayerView.playerView, newPlayerView.playerView) + companion object { private const val TAG = "ApiVideoPlayer" } diff --git a/player/src/main/java/video/api/player/extensions/WindowExtensions.kt b/player/src/main/java/video/api/player/extensions/WindowExtensions.kt new file mode 100644 index 0000000..2484678 --- /dev/null +++ b/player/src/main/java/video/api/player/extensions/WindowExtensions.kt @@ -0,0 +1,20 @@ +package video.api.player.extensions + +import android.view.Window +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat + +/** + * Hides the system UI: status bar, navigation bar and system bars. + */ +fun Window.hideSystemUI() { + WindowCompat.setDecorFitsSystemWindows(this, false) + WindowInsetsControllerCompat(this, this.decorView).let { controller -> + controller.hide(WindowInsetsCompat.Type.systemBars()) + controller.hide(WindowInsetsCompat.Type.navigationBars()) + controller.hide(WindowInsetsCompat.Type.statusBars()) + controller.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } +} \ No newline at end of file diff --git a/player/src/main/java/video/api/player/models/ApiVideoPlayerFullScreenController.kt b/player/src/main/java/video/api/player/models/ApiVideoPlayerFullScreenController.kt new file mode 100644 index 0000000..aa0e2a6 --- /dev/null +++ b/player/src/main/java/video/api/player/models/ApiVideoPlayerFullScreenController.kt @@ -0,0 +1,69 @@ +package video.api.player.models + +import android.util.Log +import android.widget.ImageButton +import androidx.fragment.app.FragmentManager +import video.api.player.ApiVideoPlayerController +import video.api.player.R +import video.api.player.views.ApiVideoExoPlayerView +import video.api.player.views.FullScreenDialogFragment + +/** + * A class that handles the player full screen. + * + * Internally, it creates another player view in a full screen dialog fragment. + * + * @param fragmentManager The fragment manager. + * @param originalPlayerView The player view. + * @param playerController The player controller. + * @param fullScreenListener The full screen listener if you want to lock the orientation in full screen. + */ +class ApiVideoPlayerFullScreenController( + private val fragmentManager: FragmentManager, + private val originalPlayerView: ApiVideoExoPlayerView, + private val playerController: ApiVideoPlayerController, + private val fullScreenListener: ApiVideoExoPlayerView.FullScreenListener? = null +) : ApiVideoExoPlayerView.FullScreenListener { + /** + * Full screen listener for the full screen player view. + */ + private val internalFullScreenListener = object : ApiVideoExoPlayerView.FullScreenListener { + override fun onFullScreenModeChanged(isFullScreen: Boolean) { + if (dialogFragment.isVisible) { + fullScreenPlayerView.playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen) + .setImageResource(R.drawable.exo_styled_controls_fullscreen_exit) + playerController.switchTargetView(fullScreenPlayerView, originalPlayerView) + dialogFragment.dismiss() + fullScreenListener?.onFullScreenModeChanged(false) + } else { + Log.e(TAG, "onFullScreenModeChanged: not expected when dialog is already visible") + } + } + } + private val fullScreenPlayerView: ApiVideoExoPlayerView = originalPlayerView.duplicate().apply { + this.fullScreenListener = this@ApiVideoPlayerFullScreenController.internalFullScreenListener + this.playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen) + .setImageResource(R.drawable.exo_styled_controls_fullscreen_exit) + } + private val dialogFragment: FullScreenDialogFragment = + FullScreenDialogFragment(fullScreenPlayerView) + + /** + * Original view full screen listener. + */ + override fun onFullScreenModeChanged(isFullScreen: Boolean) { + if (!dialogFragment.isVisible) { + originalPlayerView.playerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen) + .setImageResource(R.drawable.exo_styled_controls_fullscreen_enter) + playerController.switchTargetView(originalPlayerView, fullScreenPlayerView) + dialogFragment.show(fragmentManager, TAG) + fullScreenListener?.onFullScreenModeChanged(true) + } else { + Log.e(TAG, "onFullScreenModeChanged: not expected") + } + } + + companion object { + private const val TAG = "FullScreenDialogCtrl" + } +} \ No newline at end of file diff --git a/player/src/main/java/video/api/player/views/ApiVideoExoPlayerView.kt b/player/src/main/java/video/api/player/views/ApiVideoExoPlayerView.kt index 6803467..ebfa206 100644 --- a/player/src/main/java/video/api/player/views/ApiVideoExoPlayerView.kt +++ b/player/src/main/java/video/api/player/views/ApiVideoExoPlayerView.kt @@ -14,6 +14,7 @@ import androidx.media3.ui.PlayerView import video.api.player.R import video.api.player.databinding.ExoPlayerLayoutBinding import video.api.player.interfaces.IExoPlayerBasedPlayerView +import video.api.player.models.ApiVideoPlayerFullScreenController /** * The api.video player view class based on an ExoPlayer [PlayerView]. @@ -23,9 +24,7 @@ import video.api.player.interfaces.IExoPlayerBasedPlayerView * @param defStyleAttr an attribute in the current theme that contains a reference to a style resource that supplies default values for the view. Can be 0 to not look for defaults. */ class ApiVideoExoPlayerView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr), IExoPlayerBasedPlayerView { private val binding = ExoPlayerLayoutBinding.inflate(LayoutInflater.from(context), this) @@ -35,10 +34,11 @@ class ApiVideoExoPlayerView @JvmOverloads constructor( /** * Sets or gets the full screen listener. * If set to null, the full screen button is hidden. + * + * To simplify full screen management, you can use [ApiVideoPlayerFullScreenController]. */ var fullScreenListener: FullScreenListener? = null - @SuppressLint("UnsafeOptInUsageError") - set(value) { + @SuppressLint("UnsafeOptInUsageError") set(value) { if (value != null) { playerView.setFullscreenButtonClickListener { value.onFullScreenModeChanged(it) @@ -62,8 +62,7 @@ class ApiVideoExoPlayerView @JvmOverloads constructor( * Shows or hides the subtitles */ var showSubtitles: Boolean = true - @SuppressLint("UnsafeOptInUsageError") - set(value) { + @SuppressLint("UnsafeOptInUsageError") set(value) { if (value) { playerView.subtitleView?.visibility = VISIBLE playerView.setShowSubtitleButton(true) @@ -78,10 +77,8 @@ class ApiVideoExoPlayerView @JvmOverloads constructor( * Sets or gets how the video is fitted in its parent view */ var viewFit: ViewFit - @SuppressLint("UnsafeOptInUsageError") - get() = ViewFit.fromValue(playerView.resizeMode) - @SuppressLint("UnsafeOptInUsageError") - set(value) { + @SuppressLint("UnsafeOptInUsageError") get() = ViewFit.fromValue(playerView.resizeMode) + @SuppressLint("UnsafeOptInUsageError") set(value) { playerView.resizeMode = value.value } @@ -95,6 +92,15 @@ class ApiVideoExoPlayerView @JvmOverloads constructor( } } + fun duplicate(): ApiVideoExoPlayerView { + val view = ApiVideoExoPlayerView(context) + view.showControls = showControls + view.showSubtitles = showSubtitles + view.viewFit = viewFit + view.fullScreenListener = fullScreenListener + return view + } + interface FullScreenListener { /** * Called when the full screen button has been clicked diff --git a/player/src/main/java/video/api/player/views/FullScreenDialogFragment.kt b/player/src/main/java/video/api/player/views/FullScreenDialogFragment.kt new file mode 100644 index 0000000..4fdb6b2 --- /dev/null +++ b/player/src/main/java/video/api/player/views/FullScreenDialogFragment.kt @@ -0,0 +1,54 @@ +package video.api.player.views + +import android.os.Bundle +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.fragment.app.DialogFragment +import video.api.player.R +import video.api.player.databinding.FullscreenLayoutBinding +import video.api.player.extensions.hideSystemUI + +/** + * A full screen dialog fragment that contains a [subView]. + * + * @param subView The view to be displayed in the dialog. + */ +class FullScreenDialogFragment(private val subView: View) : + DialogFragment() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.AppTheme_FullScreenDialog) + } + + override fun onStart() { + super.onStart() + + activity?.window?.hideSystemUI() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + super.onCreateView(inflater, container, savedInstanceState) + val binding = FullscreenLayoutBinding.inflate(layoutInflater, container, false) + + if (subView.parent != null) { + (subView.parent as ViewGroup).removeView(subView) + } + subView.layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT + ).apply { + gravity = Gravity.CENTER + } + binding.container.addView(subView) + + return binding.root + } +} \ No newline at end of file diff --git a/player/src/main/res/layout/exo_player_layout.xml b/player/src/main/res/layout/exo_player_layout.xml index 513652c..fee9dd1 100644 --- a/player/src/main/res/layout/exo_player_layout.xml +++ b/player/src/main/res/layout/exo_player_layout.xml @@ -4,7 +4,7 @@ \ No newline at end of file diff --git a/player/src/main/res/layout/fullscreen_layout.xml b/player/src/main/res/layout/fullscreen_layout.xml new file mode 100644 index 0000000..3442689 --- /dev/null +++ b/player/src/main/res/layout/fullscreen_layout.xml @@ -0,0 +1,6 @@ + + \ No newline at end of file diff --git a/player/src/main/res/values/styles.xml b/player/src/main/res/values/styles.xml index 04b8f9d..f316a3c 100644 --- a/player/src/main/res/values/styles.xml +++ b/player/src/main/res/values/styles.xml @@ -8,6 +8,7 @@ + + + + + \ No newline at end of file