seperate playerView logic

This commit is contained in:
Bnyro 2022-09-09 10:51:56 +02:00
parent c3ad60f19c
commit 37dcef7fce
3 changed files with 325 additions and 290 deletions

View File

@ -46,8 +46,6 @@ import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.SubscriptionHelper import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.constants.IntentData import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.constants.PreferenceRanges
import com.github.libretube.databinding.DialogSliderBinding
import com.github.libretube.databinding.DoubleTapOverlayBinding import com.github.libretube.databinding.DoubleTapOverlayBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
import com.github.libretube.databinding.FragmentPlayerBinding import com.github.libretube.databinding.FragmentPlayerBinding
@ -61,10 +59,8 @@ import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.await import com.github.libretube.extensions.await
import com.github.libretube.extensions.formatShort import com.github.libretube.extensions.formatShort
import com.github.libretube.extensions.hideKeyboard import com.github.libretube.extensions.hideKeyboard
import com.github.libretube.extensions.setSliderRangeAndValue
import com.github.libretube.extensions.toID import com.github.libretube.extensions.toID
import com.github.libretube.interfaces.DoubleTapInterface import com.github.libretube.interfaces.PlayerViewInterface
import com.github.libretube.interfaces.PlayerOptionsInterface
import com.github.libretube.models.PlayerViewModel import com.github.libretube.models.PlayerViewModel
import com.github.libretube.obj.ChapterSegment import com.github.libretube.obj.ChapterSegment
import com.github.libretube.obj.Segment import com.github.libretube.obj.Segment
@ -77,7 +73,6 @@ import com.github.libretube.util.ImageHelper
import com.github.libretube.util.NowPlayingNotification import com.github.libretube.util.NowPlayingNotification
import com.github.libretube.util.PlayerHelper import com.github.libretube.util.PlayerHelper
import com.github.libretube.util.PreferenceHelper import com.github.libretube.util.PreferenceHelper
import com.github.libretube.views.PlayerOptionsBottomSheet
import com.google.android.exoplayer2.C import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.DefaultLoadControl import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ExoPlayer
@ -92,13 +87,11 @@ import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.MergingMediaSource import com.google.android.exoplayer2.source.MergingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.CaptionStyleCompat import com.google.android.exoplayer2.ui.CaptionStyleCompat
import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.upstream.DataSource import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultDataSource import com.google.android.exoplayer2.upstream.DefaultDataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.android.exoplayer2.util.RepeatModeUtil
import com.google.android.exoplayer2.video.VideoSize import com.google.android.exoplayer2.video.VideoSize
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -153,7 +146,6 @@ class PlayerFragment : BaseFragment() {
* for the player view * for the player view
*/ */
private lateinit var exoPlayerView: StyledPlayerView private lateinit var exoPlayerView: StyledPlayerView
private var isPlayerLocked: Boolean = false
private var subtitle = mutableListOf<SubtitleConfiguration>() private var subtitle = mutableListOf<SubtitleConfiguration>()
/** /**
@ -161,7 +153,6 @@ class PlayerFragment : BaseFragment() {
*/ */
private var token = "" private var token = ""
private var relatedStreamsEnabled = true private var relatedStreamsEnabled = true
private var autoplayEnabled = false
private var autoRotationEnabled = true private var autoRotationEnabled = true
private var playbackSpeed = "1F" private var playbackSpeed = "1F"
private var pausePlayerOnScreenOffEnabled = false private var pausePlayerOnScreenOffEnabled = false
@ -169,7 +160,6 @@ class PlayerFragment : BaseFragment() {
private var watchHistoryEnabled = true private var watchHistoryEnabled = true
private var watchPositionsEnabled = true private var watchPositionsEnabled = true
private var useSystemCaptionStyle = true private var useSystemCaptionStyle = true
private var seekIncrement = 5L
private var videoFormatPreference = "webm" private var videoFormatPreference = "webm"
private var defRes = "" private var defRes = ""
private var bufferingGoal = 50000 private var bufferingGoal = 50000
@ -178,7 +168,6 @@ class PlayerFragment : BaseFragment() {
private var sponsorBlockNotifications = true private var sponsorBlockNotifications = true
private var skipButtonsEnabled = false private var skipButtonsEnabled = false
private var pipEnabled = true private var pipEnabled = true
private var resizeModePref = "fit"
/** /**
* for autoplay * for autoplay
@ -245,7 +234,7 @@ class PlayerFragment : BaseFragment() {
* somehow the bottom bar is invisible on low screen resolutions, this fixes it * somehow the bottom bar is invisible on low screen resolutions, this fixes it
*/ */
private fun showBottomBar() { private fun showBottomBar() {
if (this::playerBinding.isInitialized && !isPlayerLocked) { if (this::playerBinding.isInitialized && !binding.player.isPlayerLocked) {
playerBinding.exoBottomBar.visibility = View.VISIBLE playerBinding.exoBottomBar.visibility = View.VISIBLE
} }
Handler(Looper.getMainLooper()).postDelayed(this::showBottomBar, 100) Handler(Looper.getMainLooper()).postDelayed(this::showBottomBar, 100)
@ -260,11 +249,7 @@ class PlayerFragment : BaseFragment() {
false false
) )
// save whether related streams and autoplay are enabled // save whether related streams are enabled
autoplayEnabled = PreferenceHelper.getBoolean(
PreferenceKeys.AUTO_PLAY,
false
)
relatedStreamsEnabled = PreferenceHelper.getBoolean( relatedStreamsEnabled = PreferenceHelper.getBoolean(
PreferenceKeys.RELATED_STREAMS, PreferenceKeys.RELATED_STREAMS,
true true
@ -300,11 +285,6 @@ class PlayerFragment : BaseFragment() {
true true
) )
seekIncrement = PreferenceHelper.getString(
PreferenceKeys.SEEK_INCREMENT,
"5"
).toLong() * 1000
videoFormatPreference = PreferenceHelper.getString( videoFormatPreference = PreferenceHelper.getString(
PreferenceKeys.PLAYER_VIDEO_FORMAT, PreferenceKeys.PLAYER_VIDEO_FORMAT,
"webm" "webm"
@ -348,11 +328,6 @@ class PlayerFragment : BaseFragment() {
PreferenceKeys.PICTURE_IN_PICTURE, PreferenceKeys.PICTURE_IN_PICTURE,
true true
) )
resizeModePref = PreferenceHelper.getString(
PreferenceKeys.PLAYER_RESIZE_MODE,
"fit"
)
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
@ -424,25 +399,7 @@ class PlayerFragment : BaseFragment() {
} }
} }
private val playerOptionsInterface = object : PlayerOptionsInterface { private val playerViewInterface = object : PlayerViewInterface {
override fun onAutoplayClicked() {
// autoplay options dialog
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.player_autoplay)
.setItems(
arrayOf(
context?.getString(R.string.enabled),
context?.getString(R.string.disabled)
)
) { _, index ->
when (index) {
0 -> autoplayEnabled = true
1 -> autoplayEnabled = false
}
}
.show()
}
override fun onCaptionClicked() { override fun onCaptionClicked() {
if (!this@PlayerFragment::streams.isInitialized || if (!this@PlayerFragment::streams.isInitialized ||
streams.subtitles == null || streams.subtitles == null ||
@ -515,62 +472,6 @@ class PlayerFragment : BaseFragment() {
} }
.show() .show()
} }
override fun onPlaybackSpeedClicked() {
val playbackSpeedBinding = DialogSliderBinding.inflate(layoutInflater)
playbackSpeedBinding.slider.setSliderRangeAndValue(
PreferenceRanges.playbackSpeed
)
playbackSpeedBinding.slider.value = exoPlayer.playbackParameters.speed
// change playback speed dialog
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.change_playback_speed)
.setView(playbackSpeedBinding.root)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.okay) { _, _ ->
exoPlayer.setPlaybackSpeed(
playbackSpeedBinding.slider.value
)
}
.show()
}
override fun onResizeModeClicked() {
// switching between original aspect ratio (black bars) and zoomed to fill device screen
val aspectRatioModeNames = context?.resources?.getStringArray(R.array.resizeMode)
val aspectRatioModes = arrayOf(
AspectRatioFrameLayout.RESIZE_MODE_FIT,
AspectRatioFrameLayout.RESIZE_MODE_ZOOM,
AspectRatioFrameLayout.RESIZE_MODE_FILL
)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.aspect_ratio)
.setItems(aspectRatioModeNames) { _, index ->
exoPlayerView.resizeMode = aspectRatioModes[index]
}
.show()
}
override fun onRepeatModeClicked() {
val repeatModeNames = arrayOf(
context?.getString(R.string.repeat_mode_none),
context?.getString(R.string.repeat_mode_current)
)
val repeatModes = arrayOf(
RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL,
RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE
)
// repeat mode options dialog
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.repeat_mode)
.setItems(repeatModeNames) { _, index ->
exoPlayer.repeatMode = repeatModes[index]
}
.show()
}
} }
// actions that don't depend on video information // actions that don't depend on video information
@ -591,55 +492,6 @@ class PlayerFragment : BaseFragment() {
.remove(this) .remove(this)
.commit() .commit()
} }
// show the advanced player options
playerBinding.toggleOptions.setOnClickListener {
val bottomSheetFragment = PlayerOptionsBottomSheet().apply {
setOnClickListeners(playerOptionsInterface)
// set the auto play mode
currentAutoplayMode = if (autoplayEnabled) {
context.getString(R.string.enabled)
} else {
context.getString(R.string.disabled)
}
// set the current caption language
currentCaptions =
if (trackSelector.parameters.preferredTextLanguages.isNotEmpty()) {
trackSelector.parameters.preferredTextLanguages[0]
} else {
context.getString(R.string.none)
}
// set the playback speed
currentPlaybackSpeed = "${
exoPlayer.playbackParameters.speed.toString()
.replace(".0", "")
}x"
// set the quality text
val isAdaptive = exoPlayer.videoFormat?.codecs != null
val quality = exoPlayer.videoSize.height
if (quality != 0) {
currentQuality =
if (isAdaptive) {
"${context.getString(R.string.hls)}${quality}p"
} else {
"${quality}p"
}
}
// set the repeat mode
currentRepeatMode =
if (exoPlayer.repeatMode == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) {
context.getString(R.string.repeat_mode_none)
} else {
context.getString(R.string.repeat_mode_current)
}
// set the aspect ratio mode
currentResizeMode = when (exoPlayerView.resizeMode) {
AspectRatioFrameLayout.RESIZE_MODE_FIT -> context.getString(R.string.resize_mode_fit)
AspectRatioFrameLayout.RESIZE_MODE_FILL -> context.getString(R.string.resize_mode_fill)
else -> context.getString(R.string.resize_mode_zoom)
}
}
bottomSheetFragment.show(childFragmentManager, null)
}
binding.playImageView.setOnClickListener { binding.playImageView.setOnClickListener {
if (!exoPlayer.isPlaying) { if (!exoPlayer.isPlaying) {
@ -677,22 +529,6 @@ class PlayerFragment : BaseFragment() {
} }
} }
// lock and unlock the player
playerBinding.lockPlayer.setOnClickListener {
// change the locked/unlocked icon
if (!isPlayerLocked) {
playerBinding.lockPlayer.setImageResource(R.drawable.ic_locked)
} else {
playerBinding.lockPlayer.setImageResource(R.drawable.ic_unlocked)
}
// show/hide all the controls
lockPlayer(isPlayerLocked)
// change locked status
isPlayerLocked = !isPlayerLocked
}
// set default playback speed // set default playback speed
exoPlayer.setPlaybackSpeed(playbackSpeed.toFloat()) exoPlayer.setPlaybackSpeed(playbackSpeed.toFloat())
@ -919,7 +755,7 @@ class PlayerFragment : BaseFragment() {
// show comments if related streams disabled // show comments if related streams disabled
if (!relatedStreamsEnabled) toggleComments() if (!relatedStreamsEnabled) toggleComments()
// prepare for autoplay // prepare for autoplay
if (autoplayEnabled) setNextStream() if (binding.player.autoplayEnabled) setNextStream()
// add the video to the watch history // add the video to the watch history
if (watchHistoryEnabled) DatabaseHelper.addToWatchHistory(videoId!!, streams) if (watchHistoryEnabled) DatabaseHelper.addToWatchHistory(videoId!!, streams)
@ -1023,11 +859,6 @@ class PlayerFragment : BaseFragment() {
controllerHideOnTouch = true controllerHideOnTouch = true
useController = false useController = false
player = exoPlayer player = exoPlayer
resizeMode = when (resizeModePref) {
"fill" -> AspectRatioFrameLayout.RESIZE_MODE_FILL
"zoom" -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
}
} }
if (useSystemCaptionStyle) { if (useSystemCaptionStyle) {
@ -1049,6 +880,14 @@ class PlayerFragment : BaseFragment() {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun initializePlayerView(response: Streams) { private fun initializePlayerView(response: Streams) {
// initialize the player view actions
binding.player.initialize(
childFragmentManager,
playerViewInterface,
doubleTapOverlayBinding,
trackSelector
)
binding.apply { binding.apply {
playerViewsInfo.text = playerViewsInfo.text =
context?.getString(R.string.views, response.views.formatShort()) + context?.getString(R.string.views, response.views.formatShort()) +
@ -1073,8 +912,6 @@ class PlayerFragment : BaseFragment() {
playerBinding.exoTitle.text = response.title playerBinding.exoTitle.text = response.title
enableDoubleTapToSeek()
// init the chapters recyclerview // init the chapters recyclerview
if (response.chapters != null) { if (response.chapters != null) {
chapters = response.chapters chapters = response.chapters
@ -1120,11 +957,11 @@ class PlayerFragment : BaseFragment() {
playbackState == Player.STATE_ENDED && playbackState == Player.STATE_ENDED &&
nextStreamId != null && nextStreamId != null &&
!transitioning && !transitioning &&
autoplayEnabled binding.player.autoplayEnabled
) { ) {
transitioning = true transitioning = true
// check whether autoplay is enabled // check whether autoplay is enabled
if (autoplayEnabled) playNextVideo() if (binding.player.autoplayEnabled) playNextVideo()
} }
if (playbackState == Player.STATE_READY) { if (playbackState == Player.STATE_READY) {
@ -1250,83 +1087,6 @@ class PlayerFragment : BaseFragment() {
} }
} }
private fun enableDoubleTapToSeek() {
// set seek increment text
val seekIncrementText = (seekIncrement / 1000).toString()
doubleTapOverlayBinding.rewindTV.text = seekIncrementText
doubleTapOverlayBinding.forwardTV.text = seekIncrementText
binding.player.setOnDoubleTapListener(
object : DoubleTapInterface {
override fun onEvent(x: Float) {
val width = exoPlayerView.width
when {
width * 0.5 > x -> rewind()
width * 0.5 < x -> forward()
}
}
}
)
}
private fun rewind() {
exoPlayer.seekTo(exoPlayer.currentPosition - seekIncrement)
// show the rewind button
doubleTapOverlayBinding.rewindBTN.apply {
visibility = View.VISIBLE
// clear previous animation
animate().rotation(0F).setDuration(0).start()
// start new animation
animate()
.rotation(-30F)
.setDuration(100)
.withEndAction {
// reset the animation when finished
animate().rotation(0F).setDuration(100).start()
}
.start()
removeCallbacks(hideRewindButtonRunnable)
// start callback to hide the button
postDelayed(hideRewindButtonRunnable, 700)
}
}
private fun forward() {
exoPlayer.seekTo(exoPlayer.currentPosition + seekIncrement)
// show the forward button
doubleTapOverlayBinding.forwardBTN.apply {
visibility = View.VISIBLE
// clear previous animation
animate().rotation(0F).setDuration(0).start()
// start new animation
animate()
.rotation(30F)
.setDuration(100)
.withEndAction {
// reset the animation when finished
animate().rotation(0F).setDuration(100).start()
}
.start()
// start callback to hide the button
removeCallbacks(hideForwardButtonRunnable)
postDelayed(hideForwardButtonRunnable, 700)
}
}
private val hideForwardButtonRunnable = Runnable {
doubleTapOverlayBinding.forwardBTN.apply {
visibility = View.GONE
}
}
private val hideRewindButtonRunnable = Runnable {
doubleTapOverlayBinding.rewindBTN.apply {
visibility = View.GONE
}
}
private fun initializeChapters() { private fun initializeChapters() {
if (chapters.isEmpty()) { if (chapters.isEmpty()) {
binding.chaptersRecView.visibility = View.GONE binding.chaptersRecView.visibility = View.GONE
@ -1533,7 +1293,7 @@ class PlayerFragment : BaseFragment() {
// handles the audio focus // handles the audio focus
val audioAttributes = AudioAttributes.Builder() val audioAttributes = AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA) .setUsage(C.USAGE_MEDIA)
.setContentType(C.CONTENT_TYPE_MOVIE) .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.build() .build()
// handles the duration of media to retain in the buffer prior to the current playback position (for fast backward seeking) // handles the duration of media to retain in the buffer prior to the current playback position (for fast backward seeking)
@ -1570,34 +1330,6 @@ class PlayerFragment : BaseFragment() {
nowPlayingNotification.updatePlayerNotification(streams) nowPlayingNotification.updatePlayerNotification(streams)
} }
// lock the player
private fun lockPlayer(isLocked: Boolean) {
// isLocked is the current (old) state of the player lock
val visibility = if (isLocked) View.VISIBLE else View.GONE
playerBinding.exoTopBarRight.visibility = visibility
playerBinding.exoCenterControls.visibility = visibility
playerBinding.exoBottomBar.visibility = visibility
playerBinding.closeImageButton.visibility = visibility
playerBinding.exoTitle.visibility =
if (isLocked &&
viewModel.isFullscreen.value == true
) {
View.VISIBLE
} else {
View.INVISIBLE
}
// disable double tap to seek when the player is locked
if (isLocked) {
// enable fast forward and rewind by double tapping
enableDoubleTapToSeek()
} else {
// disable fast forward and rewind by double tapping
binding.player.setOnDoubleTapListener(null)
}
}
private fun isSubscribed() { private fun isSubscribed() {
fun run() { fun run() {
val channelId = streams.uploaderUrl!!.toID() val channelId = streams.uploaderUrl!!.toID()

View File

@ -0,0 +1,7 @@
package com.github.libretube.interfaces
interface PlayerViewInterface {
fun onCaptionClicked()
fun onQualityClicked()
}

View File

@ -3,12 +3,28 @@ package com.github.libretube.views
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View
import androidx.fragment.app.FragmentManager
import com.github.libretube.R
import com.github.libretube.activities.MainActivity import com.github.libretube.activities.MainActivity
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.constants.PreferenceRanges
import com.github.libretube.databinding.DialogSliderBinding
import com.github.libretube.databinding.DoubleTapOverlayBinding
import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding import com.github.libretube.databinding.ExoStyledPlayerControlViewBinding
import com.github.libretube.extensions.setSliderRangeAndValue
import com.github.libretube.interfaces.DoubleTapInterface import com.github.libretube.interfaces.DoubleTapInterface
import com.github.libretube.interfaces.PlayerOptionsInterface
import com.github.libretube.interfaces.PlayerViewInterface
import com.github.libretube.util.DoubleTapListener import com.github.libretube.util.DoubleTapListener
import com.github.libretube.util.PreferenceHelper
import com.google.android.exoplayer2.trackselection.TrackSelector
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.util.RepeatModeUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
internal class CustomExoPlayerView( internal class CustomExoPlayerView(
@ -16,17 +32,36 @@ internal class CustomExoPlayerView(
attributeSet: AttributeSet? = null attributeSet: AttributeSet? = null
) : StyledPlayerView(context, attributeSet) { ) : StyledPlayerView(context, attributeSet) {
val binding: ExoStyledPlayerControlViewBinding = ExoStyledPlayerControlViewBinding.bind(this) val binding: ExoStyledPlayerControlViewBinding = ExoStyledPlayerControlViewBinding.bind(this)
private var doubleTapOverlayBinding: DoubleTapOverlayBinding? = null
private var doubleTapListener: DoubleTapInterface? = null private var doubleTapListener: DoubleTapInterface? = null
private var playerViewInterface: PlayerViewInterface? = null
private lateinit var childFragmentManager: FragmentManager
private lateinit var trackSelector: TrackSelector
// the x-position of where the user clicked // the x-position of where the user clicked
private var xPos = 0F private var xPos = 0F
fun setOnDoubleTapListener( var isPlayerLocked: Boolean = false
eventListener: DoubleTapInterface?
) { /**
doubleTapListener = eventListener * Preferences
} */
var autoplayEnabled = PreferenceHelper.getBoolean(
PreferenceKeys.AUTO_PLAY,
false
)
private val seekIncrement = PreferenceHelper.getString(
PreferenceKeys.SEEK_INCREMENT,
"5"
).toLong() * 1000
private var resizeModePref = PreferenceHelper.getString(
PreferenceKeys.PLAYER_RESIZE_MODE,
"fit"
)
private fun toggleController() { private fun toggleController() {
if (isControllerFullyVisible) hideController() else showController() if (isControllerFullyVisible) hideController() else showController()
@ -42,9 +77,44 @@ internal class CustomExoPlayerView(
} }
} }
init { fun initialize(
childFragmentManager: FragmentManager,
playerViewInterface: PlayerViewInterface,
doubleTapOverlayBinding: DoubleTapOverlayBinding,
trackSelector: TrackSelector
) {
this.childFragmentManager = childFragmentManager
this.playerViewInterface = playerViewInterface
this.doubleTapOverlayBinding = doubleTapOverlayBinding
this.trackSelector = trackSelector
// set the double click listener for rewind/forward // set the double click listener for rewind/forward
setOnClickListener(doubleTouchListener) setOnClickListener(doubleTouchListener)
enableDoubleTapToSeek()
initializeAdvancedOptions()
// locking the player
binding.lockPlayer.setOnClickListener {
// change the locked/unlocked icon
binding.lockPlayer.setImageResource(
if (!isPlayerLocked) R.drawable.ic_locked
else R.drawable.ic_unlocked
)
// show/hide all the controls
lockPlayer(isPlayerLocked)
// change locked status
isPlayerLocked = !isPlayerLocked
}
resizeMode = when (resizeModePref) {
"fill" -> AspectRatioFrameLayout.RESIZE_MODE_FILL
"zoom" -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
else -> AspectRatioFrameLayout.RESIZE_MODE_FIT
}
} }
override fun hideController() { override fun hideController() {
@ -59,4 +129,230 @@ internal class CustomExoPlayerView(
doubleTouchListener.onClick(this) doubleTouchListener.onClick(this)
return false return false
} }
private fun initializeAdvancedOptions() {
binding.toggleOptions.setOnClickListener {
val bottomSheetFragment = PlayerOptionsBottomSheet().apply {
setOnClickListeners(playerOptionsInterface)
// set the auto play mode
currentAutoplayMode = if (autoplayEnabled) {
context?.getString(R.string.enabled)
} else {
context?.getString(R.string.disabled)
}
// set the current caption language
currentCaptions =
if (trackSelector.parameters.preferredTextLanguages.isNotEmpty()) {
trackSelector.parameters.preferredTextLanguages[0]
} else {
context?.getString(R.string.none)
}
// set the playback speed
currentPlaybackSpeed = "${
player?.playbackParameters?.speed.toString()
.replace(".0", "")
}x"
// set the quality text
val quality = player?.videoSize?.height
if (quality != 0) {
currentQuality = "${quality}p"
}
// set the repeat mode
currentRepeatMode =
if (player?.repeatMode == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) {
context?.getString(R.string.repeat_mode_none)
} else {
context?.getString(R.string.repeat_mode_current)
}
// set the aspect ratio mode
currentResizeMode = when (resizeMode) {
AspectRatioFrameLayout.RESIZE_MODE_FIT -> context?.getString(R.string.resize_mode_fit)
AspectRatioFrameLayout.RESIZE_MODE_FILL -> context?.getString(R.string.resize_mode_fill)
else -> context?.getString(R.string.resize_mode_zoom)
}
}
bottomSheetFragment.show(childFragmentManager, null)
}
}
// lock the player
private fun lockPlayer(isLocked: Boolean) {
// isLocked is the current (old) state of the player lock
val visibility = if (isLocked) View.VISIBLE else View.GONE
binding.exoTopBarRight.visibility = visibility
binding.exoCenterControls.visibility = visibility
binding.exoBottomBar.visibility = visibility
binding.closeImageButton.visibility = visibility
// disable double tap to seek when the player is locked
if (isLocked) {
// enable fast forward and rewind by double tapping
enableDoubleTapToSeek()
} else {
// disable fast forward and rewind by double tapping
doubleTapListener = null
}
}
private fun enableDoubleTapToSeek() {
// set seek increment text
val seekIncrementText = (seekIncrement / 1000).toString()
doubleTapOverlayBinding?.rewindTV?.text = seekIncrementText
doubleTapOverlayBinding?.forwardTV?.text = seekIncrementText
doubleTapListener =
object : DoubleTapInterface {
override fun onEvent(x: Float) {
when {
width * 0.5 > x -> rewind()
width * 0.5 < x -> forward()
}
}
}
}
private fun rewind() {
player?.seekTo((player?.currentPosition ?: 0L) - seekIncrement)
// show the rewind button
doubleTapOverlayBinding?.rewindBTN.apply {
visibility = View.VISIBLE
// clear previous animation
animate().rotation(0F).setDuration(0).start()
// start new animation
animate()
.rotation(-30F)
.setDuration(100)
.withEndAction {
// reset the animation when finished
animate().rotation(0F).setDuration(100).start()
}
.start()
removeCallbacks(hideRewindButtonRunnable)
// start callback to hide the button
postDelayed(hideRewindButtonRunnable, 700)
}
}
private fun forward() {
player?.seekTo(player!!.currentPosition + seekIncrement)
// show the forward button
doubleTapOverlayBinding?.forwardBTN.apply {
visibility = View.VISIBLE
// clear previous animation
animate().rotation(0F).setDuration(0).start()
// start new animation
animate()
.rotation(30F)
.setDuration(100)
.withEndAction {
// reset the animation when finished
animate().rotation(0F).setDuration(100).start()
}
.start()
// start callback to hide the button
removeCallbacks(hideForwardButtonRunnable)
postDelayed(hideForwardButtonRunnable, 700)
}
}
private val hideForwardButtonRunnable = Runnable {
doubleTapOverlayBinding?.forwardBTN.apply {
visibility = View.GONE
}
}
private val hideRewindButtonRunnable = Runnable {
doubleTapOverlayBinding?.rewindBTN.apply {
visibility = View.GONE
}
}
private val playerOptionsInterface = object : PlayerOptionsInterface {
override fun onAutoplayClicked() {
// autoplay options dialog
MaterialAlertDialogBuilder(context)
.setTitle(R.string.player_autoplay)
.setItems(
arrayOf(
context.getString(R.string.enabled),
context.getString(R.string.disabled)
)
) { _, index ->
when (index) {
0 -> autoplayEnabled = true
1 -> autoplayEnabled = false
}
}
.show()
}
override fun onCaptionClicked() {
playerViewInterface?.onCaptionClicked()
}
override fun onQualityClicked() {
playerViewInterface?.onQualityClicked()
}
override fun onPlaybackSpeedClicked() {
val playbackSpeedBinding = DialogSliderBinding.inflate(
LayoutInflater.from(context)
)
playbackSpeedBinding.slider.setSliderRangeAndValue(
PreferenceRanges.playbackSpeed
)
playbackSpeedBinding.slider.value = player?.playbackParameters?.speed ?: 1f
// change playback speed dialog
MaterialAlertDialogBuilder(context)
.setTitle(R.string.change_playback_speed)
.setView(playbackSpeedBinding.root)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.okay) { _, _ ->
player?.setPlaybackSpeed(
playbackSpeedBinding.slider.value
)
}
.show()
}
override fun onResizeModeClicked() {
// switching between original aspect ratio (black bars) and zoomed to fill device screen
val aspectRatioModeNames = context.resources?.getStringArray(R.array.resizeMode)
val aspectRatioModes = arrayOf(
AspectRatioFrameLayout.RESIZE_MODE_FIT,
AspectRatioFrameLayout.RESIZE_MODE_ZOOM,
AspectRatioFrameLayout.RESIZE_MODE_FILL
)
MaterialAlertDialogBuilder(context)
.setTitle(R.string.aspect_ratio)
.setItems(aspectRatioModeNames) { _, index ->
resizeMode = aspectRatioModes[index]
}
.show()
}
override fun onRepeatModeClicked() {
val repeatModeNames = arrayOf(
context.getString(R.string.repeat_mode_none),
context.getString(R.string.repeat_mode_current)
)
val repeatModes = arrayOf(
RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL,
RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE
)
// repeat mode options dialog
MaterialAlertDialogBuilder(context)
.setTitle(R.string.repeat_mode)
.setItems(repeatModeNames) { _, index ->
player?.repeatMode = repeatModes[index]
}
.show()
}
}
} }