LibreTube/app/src/main/java/com/github/libretube/views/CustomExoPlayerView.kt
2022-09-10 16:47:45 +02:00

383 lines
14 KiB
Kotlin

package com.github.libretube.views
import android.annotation.SuppressLint
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.view.LayoutInflater
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.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.extensions.setSliderRangeAndValue
import com.github.libretube.interfaces.DoubleTapInterface
import com.github.libretube.interfaces.PlayerOptionsInterface
import com.github.libretube.obj.BottomSheetItem
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.util.RepeatModeUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@SuppressLint("ClickableViewAccessibility")
internal class CustomExoPlayerView(
context: Context,
attributeSet: AttributeSet? = null
) : StyledPlayerView(context, attributeSet) {
val binding: ExoStyledPlayerControlViewBinding = ExoStyledPlayerControlViewBinding.bind(this)
private var doubleTapOverlayBinding: DoubleTapOverlayBinding? = null
/**
* Objects from the parent fragment
*/
private var doubleTapListener: DoubleTapInterface? = null
private var playerOptionsInterface: PlayerOptionsInterface? = null
private lateinit var childFragmentManager: FragmentManager
private var trackSelector: TrackSelector? = null
private val runnableHandler = Handler(Looper.getMainLooper())
// the x-position of where the user clicked
private var xPos = 0F
var isPlayerLocked: Boolean = false
/**
* 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() {
if (isControllerFullyVisible) hideController() else showController()
}
private val doubleTouchListener = object : DoubleTapListener() {
override fun onDoubleClick() {
doubleTapListener?.onEvent(xPos)
}
override fun onSingleClick() {
toggleController()
}
}
fun initialize(
childFragmentManager: FragmentManager,
playerViewInterface: PlayerOptionsInterface?,
doubleTapOverlayBinding: DoubleTapOverlayBinding,
trackSelector: TrackSelector?
) {
this.childFragmentManager = childFragmentManager
this.playerOptionsInterface = playerViewInterface
this.doubleTapOverlayBinding = doubleTapOverlayBinding
this.trackSelector = trackSelector
// set the double click listener for rewind/forward
setOnClickListener(doubleTouchListener)
enableDoubleTapToSeek()
initializeAdvancedOptions(context)
// 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() {
(context as? MainActivity)?.hideSystemBars()
super.hideController()
}
override fun onTouchEvent(event: MotionEvent): Boolean {
// save the x position of the touch event
xPos = event.x
// listen for a double touch
doubleTouchListener.onClick(this)
return false
}
private fun initializeAdvancedOptions(context: Context) {
binding.toggleOptions.setOnClickListener {
val bottomSheetFragment = BottomSheet().apply {
val items = listOf(
BottomSheetItem(
context.getString(R.string.player_autoplay),
R.drawable.ic_play,
if (autoplayEnabled) {
context.getString(R.string.enabled)
} else {
context.getString(R.string.disabled)
}
),
BottomSheetItem(
context.getString(R.string.playback_speed),
R.drawable.ic_speed,
"${player?.playbackParameters?.speed
.toString()
.replace(".0", "")
}x"
),
BottomSheetItem(
context.getString(R.string.repeat_mode),
R.drawable.ic_repeat,
if (player?.repeatMode == RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE) {
context.getString(R.string.repeat_mode_none)
} else {
context.getString(R.string.repeat_mode_current)
}
),
BottomSheetItem(
context.getString(R.string.player_resize_mode),
R.drawable.ic_aspect_ratio,
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)
}
),
BottomSheetItem(
context.getString(R.string.quality),
R.drawable.ic_hd,
"${player?.videoSize?.height}p"
),
BottomSheetItem(
context.getString(R.string.captions),
R.drawable.ic_caption,
if (trackSelector != null && trackSelector!!.parameters.preferredTextLanguages.isNotEmpty()) {
trackSelector!!.parameters.preferredTextLanguages[0]
} else {
context.getString(R.string.none)
}
)
)
setItems(items) { index ->
when (index) {
0 -> onAutoplayClicked()
1 -> onPlaybackSpeedClicked()
2 -> onRepeatModeClicked()
3 -> onResizeModeClicked()
4 -> playerOptionsInterface?.onQualityClicked()
5 -> playerOptionsInterface?.onCaptionClicked()
}
}
}
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 {
this!!.visibility = View.VISIBLE
// clear previous animation
this.animate().rotation(0F).setDuration(0).start()
// start new animation
this.animate()
.rotation(-30F)
.setDuration(100)
.withEndAction {
// reset the animation when finished
animate().rotation(0F).setDuration(100).start()
}
.start()
runnableHandler.removeCallbacks(hideRewindButtonRunnable)
// start callback to hide the button
runnableHandler.postDelayed(hideRewindButtonRunnable, 700)
}
}
private fun forward() {
player?.seekTo(player!!.currentPosition + seekIncrement)
// show the forward button
doubleTapOverlayBinding?.forwardBTN.apply {
this!!.visibility = View.VISIBLE
// clear previous animation
this.animate().rotation(0F).setDuration(0).start()
// start new animation
this.animate()
.rotation(30F)
.setDuration(100)
.withEndAction {
// reset the animation when finished
animate().rotation(0F).setDuration(100).start()
}
.start()
// start callback to hide the button
runnableHandler.removeCallbacks(hideForwardButtonRunnable)
runnableHandler.postDelayed(hideForwardButtonRunnable, 700)
}
}
private val hideForwardButtonRunnable = Runnable {
doubleTapOverlayBinding?.forwardBTN.apply {
this!!.visibility = View.GONE
}
}
private val hideRewindButtonRunnable = Runnable {
doubleTapOverlayBinding?.rewindBTN.apply {
this!!.visibility = View.GONE
}
}
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()
}
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()
}
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()
}
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_NONE,
RepeatModeUtil.REPEAT_TOGGLE_MODE_ALL
)
// repeat mode options dialog
MaterialAlertDialogBuilder(context)
.setTitle(R.string.repeat_mode)
.setItems(repeatModeNames) { _, index ->
player?.repeatMode = repeatModes[index]
}
.show()
}
}