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