attempt to declutter the player activity and layout

This commit is contained in:
jmir1 2022-04-14 11:53:31 +02:00
parent 0890a21eb2
commit 6c7e012e40
4 changed files with 668 additions and 603 deletions

View file

@ -29,12 +29,8 @@ import android.view.View
import android.view.ViewAnimationUtils import android.view.ViewAnimationUtils
import android.view.WindowManager import android.view.WindowManager
import android.view.animation.AnimationUtils import android.view.animation.AnimationUtils
import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.SeekBar
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.GestureDetectorCompat import androidx.core.view.GestureDetectorCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
@ -54,9 +50,6 @@ import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import `is`.xyz.mpv.MPVLib import `is`.xyz.mpv.MPVLib
import `is`.xyz.mpv.PickerDialog
import `is`.xyz.mpv.SpeedPickerDialog
import `is`.xyz.mpv.StateRestoreCallback
import `is`.xyz.mpv.Utils import `is`.xyz.mpv.Utils
import logcat.LogPriority import logcat.LogPriority
import nucleus.factory.RequiresPresenter import nucleus.factory.RequiresPresenter
@ -85,17 +78,6 @@ class PlayerActivity :
super.onNewIntent(intent) super.onNewIntent(intent)
} }
private val ACTION_MEDIA_CONTROL = "media_control"
private val EXTRA_CONTROL_TYPE = "control_type"
private val REQUEST_PLAY = 1
private val REQUEST_PAUSE = 2
private val CONTROL_TYPE_PLAY = 1
private val CONTROL_TYPE_PAUSE = 2
private val REQUEST_PREVIOUS = 3
private val REQUEST_NEXT = 4
private val CONTROL_TYPE_PREVIOUS = 3
private val CONTROL_TYPE_NEXT = 4
private var isInPipMode: Boolean = false private var isInPipMode: Boolean = false
private var mReceiver: BroadcastReceiver? = null private var mReceiver: BroadcastReceiver? = null
@ -104,7 +86,7 @@ class PlayerActivity :
private val langName = LocaleHelper.getSimpleLocaleDisplay(preferences.lang().get()) private val langName = LocaleHelper.getSimpleLocaleDisplay(preferences.lang().get())
private val player get() = binding.player internal val player get() = binding.player
private var audioManager: AudioManager? = null private var audioManager: AudioManager? = null
private var fineVolume = 0F private var fineVolume = 0F
@ -117,7 +99,7 @@ class PlayerActivity :
internal var isLocked = false internal var isLocked = false
private val windowInsetsController by lazy { WindowInsetsControllerCompat(window, binding.root) } internal val windowInsetsController by lazy { WindowInsetsControllerCompat(window, binding.root) }
private var audioFocusRestore: () -> Unit = {} private var audioFocusRestore: () -> Unit = {}
@ -192,8 +174,6 @@ class PlayerActivity :
private var initialSeek = -1 private var initialSeek = -1
private var userIsOperatingSeekbar = false
private lateinit var mDetector: GestureDetectorCompat private lateinit var mDetector: GestureDetectorCompat
private val animationHandler = Handler(Looper.getMainLooper()) private val animationHandler = Handler(Looper.getMainLooper())
@ -201,7 +181,7 @@ class PlayerActivity :
// Fade out seek text // Fade out seek text
private val seekTextRunnable = Runnable { private val seekTextRunnable = Runnable {
AnimationUtils.loadAnimation(this, R.anim.fade_out_medium).also { fadeAnimation -> AnimationUtils.loadAnimation(this, R.anim.fade_out_medium).also { fadeAnimation ->
findViewById<LinearLayout>(R.id.seekView).startAnimation(fadeAnimation) binding.seekView.startAnimation(fadeAnimation)
binding.seekView.visibility = View.GONE binding.seekView.visibility = View.GONE
} }
} }
@ -209,7 +189,7 @@ class PlayerActivity :
// Fade out Volume Bar // Fade out Volume Bar
private val volumeViewRunnable = Runnable { private val volumeViewRunnable = Runnable {
AnimationUtils.loadAnimation(this, R.anim.fade_out_medium).also { fadeAnimation -> AnimationUtils.loadAnimation(this, R.anim.fade_out_medium).also { fadeAnimation ->
findViewById<LinearLayout>(R.id.volumeView).startAnimation(fadeAnimation) binding.volumeView.startAnimation(fadeAnimation)
binding.volumeView.visibility = View.GONE binding.volumeView.visibility = View.GONE
} }
} }
@ -217,24 +197,11 @@ class PlayerActivity :
// Fade out Brightness Bar // Fade out Brightness Bar
private val brightnessViewRunnable = Runnable { private val brightnessViewRunnable = Runnable {
AnimationUtils.loadAnimation(this, R.anim.fade_out_medium).also { fadeAnimation -> AnimationUtils.loadAnimation(this, R.anim.fade_out_medium).also { fadeAnimation ->
findViewById<LinearLayout>(R.id.brightnessView).startAnimation(fadeAnimation) binding.brightnessView.startAnimation(fadeAnimation)
binding.brightnessView.visibility = View.GONE binding.brightnessView.visibility = View.GONE
} }
} }
// Fade out Player controls
private val controlsViewRunnable = Runnable {
AnimationUtils.loadAnimation(this, R.anim.fade_out_medium).also { fadeAnimation ->
if (!isLocked) {
findViewById<LinearLayout>(R.id.controlsView).startAnimation(fadeAnimation)
binding.controlsView.visibility = View.GONE
} else {
findViewById<LinearLayout>(R.id.lockedView).startAnimation(fadeAnimation)
binding.lockedView.visibility = View.GONE
}
}
}
private fun showGestureView(type: String) { private fun showGestureView(type: String) {
val callback: Runnable val callback: Runnable
val itemView: LinearLayout val itemView: LinearLayout
@ -255,11 +222,6 @@ class PlayerActivity :
itemView = binding.brightnessView itemView = binding.brightnessView
delay = 500L delay = 500L
} }
"controls" -> {
callback = controlsViewRunnable
itemView = if (!isLocked) binding.controlsView else binding.lockedView
delay = 3500L
}
else -> return else -> return
} }
@ -268,39 +230,21 @@ class PlayerActivity :
animationHandler.postDelayed(callback, delay) animationHandler.postDelayed(callback, delay)
} }
private val seekBarChangeListener = object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (!fromUser) {
return
}
player.timePos = progress
updatePlaybackPos(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
userIsOperatingSeekbar = true
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
userIsOperatingSeekbar = false
}
}
private var currentVideoList: List<Video>? = null private var currentVideoList: List<Video>? = null
private var playerViewMode: Int = preferences.getPlayerViewMode() private var playerViewMode: Int = preferences.getPlayerViewMode()
private var playerIsDestroyed = true private var playerIsDestroyed = true
private var subTracks: Array<Track> = emptyArray() internal var subTracks: Array<Track> = emptyArray()
private var selectedSub = 0 internal var selectedSub = 0
private var hadPreviousSubs = false private var hadPreviousSubs = false
private var audioTracks: Array<Track> = emptyArray() internal var audioTracks: Array<Track> = emptyArray()
private var selectedAudio = 0 internal var selectedAudio = 0
private var hadPreviousAudio = false private var hadPreviousAudio = false
@ -320,7 +264,7 @@ class PlayerActivity :
} }
setVisibilities() setVisibilities()
showGestureView("controls") binding.playerControls.showAndFadeControls()
player.initialize(applicationContext.filesDir.path) player.initialize(applicationContext.filesDir.path)
MPVLib.setOptionString("keep-open", "always") MPVLib.setOptionString("keep-open", "always")
@ -351,29 +295,6 @@ class PlayerActivity :
mDetector.onTouchEvent(event) mDetector.onTouchEvent(event)
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
binding.pipBtn.isVisible = packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
}
binding.backArrowBtn.setOnClickListener { finishAndRemoveTask() }
binding.pipBtn.setOnClickListener { startPiP() }
// Lock and Unlock controls
binding.lockBtn.setOnClickListener { isLocked = true; toggleControls() }
binding.unlockBtn.setOnClickListener { isLocked = false; toggleControls() }
// Cycle, Long click controls
binding.cycleAudioBtn.setOnLongClickListener { pickAudio(); true }
binding.cycleSpeedBtn.setOnLongClickListener { pickSpeed(); true }
binding.cycleSubsBtn.setOnLongClickListener { pickSub(); true }
binding.playbackSeekbar.setOnSeekBarChangeListener(seekBarChangeListener)
// player.playFile(currentVideoList!!.first().videoUrl!!.toString())
binding.nextBtn.setOnClickListener { switchEpisode(false) }
binding.prevBtn.setOnClickListener { switchEpisode(true) }
if (presenter?.needsInit() == true) { if (presenter?.needsInit() == true) {
val anime = intent.extras!!.getLong("anime", -1) val anime = intent.extras!!.getLong("anime", -1)
val episode = intent.extras!!.getLong("episode", -1) val episode = intent.extras!!.getLong("episode", -1)
@ -391,7 +312,7 @@ class PlayerActivity :
* Switches to the previous episode if [previous] is true, * Switches to the previous episode if [previous] is true,
* to the next episode if [previous] is false * to the next episode if [previous] is false
*/ */
private fun switchEpisode(previous: Boolean) { internal fun switchEpisode(previous: Boolean) {
val switchMethod = if (previous) presenter::previousEpisode else presenter::nextEpisode val switchMethod = if (previous) presenter::previousEpisode else presenter::nextEpisode
val errorRes = if (previous) R.string.no_previous_episode else R.string.no_next_episode val errorRes = if (previous) R.string.no_previous_episode else R.string.no_next_episode
@ -414,92 +335,15 @@ class PlayerActivity :
} }
} }
fun toggleControls() { fun toggleControls() = binding.playerControls.toggleControls()
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
if (isLocked) {
// Hide controls
binding.controlsView.isVisible = false
if (!binding.lockedView.isVisible && !player.paused!!) showGestureView("controls")
else if (!binding.lockedView.isVisible && player.paused!!) binding.lockedView.visibility = View.VISIBLE
else {
animationHandler.removeCallbacks(controlsViewRunnable)
AnimationUtils.loadAnimation(this, R.anim.fade_out_medium).also { fadeAnimation ->
findViewById<LinearLayout>(R.id.lockedView).startAnimation(fadeAnimation)
binding.lockedView.visibility = View.GONE
}
}
} else {
if (!binding.controlsView.isVisible && !player.paused!!) showGestureView("controls")
else if (!binding.controlsView.isVisible && player.paused!!) binding.controlsView.visibility = View.VISIBLE
else {
animationHandler.removeCallbacks(controlsViewRunnable)
AnimationUtils.loadAnimation(this, R.anim.fade_out_medium).also { fadeAnimation ->
findViewById<LinearLayout>(R.id.controlsView).startAnimation(fadeAnimation)
binding.controlsView.visibility = View.GONE
}
}
binding.lockedView.isVisible = false
}
}
private fun hideControls(hide: Boolean) {
binding.controlsView.isVisible = !hide
}
private fun showLoadingIndicator(visible: Boolean) { private fun showLoadingIndicator(visible: Boolean) {
if (binding.loadingIndicator.isVisible == visible) return if (binding.loadingIndicator.isVisible == visible) return
binding.playBtn.isVisible = !visible binding.playerControls.binding.playBtn.isVisible = !visible
binding.loadingIndicator.isVisible = visible binding.loadingIndicator.isVisible = visible
} }
private fun pickAudio() { internal fun setSub(index: Int) {
val restore = pauseForDialog()
with(MaterialAlertDialogBuilder(this)) {
setSingleChoiceItems(
audioTracks.map { it.lang }.toTypedArray(),
selectedAudio,
) { dialog, item ->
if (item == selectedSub) return@setSingleChoiceItems
if (item == 0) {
selectedAudio = 0
player.aid = -1
return@setSingleChoiceItems
}
setAudio(item)
dialog.dismiss()
}
setOnDismissListener { restore() }
create().show()
}
}
private fun pickSub() {
val restore = pauseForDialog()
with(MaterialAlertDialogBuilder(this)) {
setSingleChoiceItems(
subTracks.map { it.lang }.toTypedArray(),
selectedSub,
) { dialog, item ->
if (item == 0) {
selectedSub = 0
player.sid = -1
return@setSingleChoiceItems
}
setSub(item)
dialog.dismiss()
}
setOnDismissListener { restore() }
create().show()
}
}
private fun setSub(index: Int) {
if (selectedSub == index || selectedSub > subTracks.lastIndex) return if (selectedSub == index || selectedSub > subTracks.lastIndex) return
selectedSub = index selectedSub = index
if (index == 0) { if (index == 0) {
@ -515,7 +359,7 @@ class PlayerActivity :
?: MPVLib.command(arrayOf("sub-add", subTracks[index].url, "select", subTracks[index].url)) ?: MPVLib.command(arrayOf("sub-add", subTracks[index].url, "select", subTracks[index].url))
} }
private fun setAudio(index: Int) { internal fun setAudio(index: Int) {
if (selectedAudio == index || selectedAudio > audioTracks.lastIndex) return if (selectedAudio == index || selectedAudio > audioTracks.lastIndex) return
selectedAudio = index selectedAudio = index
if (index == 0) { if (index == 0) {
@ -531,53 +375,6 @@ class PlayerActivity :
?: MPVLib.command(arrayOf("audio-add", audioTracks[index].url, "select", audioTracks[index].url)) ?: MPVLib.command(arrayOf("audio-add", audioTracks[index].url, "select", audioTracks[index].url))
} }
private fun pauseForDialog(): StateRestoreCallback {
val wasPlayerPaused = player.paused ?: true // default to not changing state
player.paused = true
return {
if (!wasPlayerPaused) {
player.paused = false
}
}
}
private fun pickSpeed() {
// TODO: replace this with SliderPickerDialog
val picker = SpeedPickerDialog()
val restore = pauseForDialog()
speedPickerDialog(picker, R.string.title_speed_dialog) {
updateSpeedButton()
restore()
}
}
private fun speedPickerDialog(
picker: PickerDialog,
@StringRes titleRes: Int,
restoreState: StateRestoreCallback,
) {
val dialog = with(AlertDialog.Builder(this)) {
setTitle(titleRes)
setView(picker.buildView(layoutInflater))
setPositiveButton(R.string.dialog_ok) { _, _ ->
picker.number?.let {
if (picker.isInteger()) {
MPVLib.setPropertyInt("speed", it.toInt())
} else {
MPVLib.setPropertyDouble("speed", it)
}
}
}
setNegativeButton(R.string.dialog_cancel) { dialog, _ -> dialog.cancel() }
setOnDismissListener { restoreState() }
create()
}
picker.number = MPVLib.getPropertyDouble("speed")
dialog.show()
}
private fun setViewMode() { private fun setViewMode() {
when (playerViewMode) { when (playerViewMode) {
2 -> { 2 -> {
@ -610,67 +407,35 @@ class PlayerActivity :
} }
} }
fun updatePlaybackPos(position: Int) {
binding.playbackPositionTxt.text = Utils.prettyTime(position)
if (!userIsOperatingSeekbar) {
binding.playbackSeekbar.progress = position
}
updateDecoderButton()
updateSpeedButton()
}
private fun updatePlaybackDuration(duration: Int) {
binding.playbackDurationTxt.text = Utils.prettyTime(duration)
if (!userIsOperatingSeekbar) {
binding.playbackSeekbar.max = duration
}
}
private fun updateDecoderButton() {
if (binding.cycleDecoderBtn.visibility != View.VISIBLE) {
return
}
binding.cycleDecoderBtn.text = if (player.hwdecActive) "HW" else "SW"
}
private fun updateSpeedButton() {
binding.cycleSpeedBtn.text = getString(R.string.ui_speed, player.playbackSpeed)
}
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun playPause(view: View) { fun playPause(view: View) {
player.cyclePause() player.cyclePause()
when { binding.playerControls.playPause()
player.paused!! -> animationHandler.removeCallbacks(controlsViewRunnable)
binding.controlsView.isVisible -> showGestureView("controls")
}
} }
val playPauseRunnable = Runnable { private val doubleTapPlayPauseRunnable = Runnable {
AnimationUtils.loadAnimation(this, R.anim.fade_out_medium).also { fadeAnimation -> AnimationUtils.loadAnimation(this, R.anim.fade_out_medium).also { fadeAnimation ->
findViewById<ImageView>(R.id.playPauseView).startAnimation(fadeAnimation) binding.playPauseView.startAnimation(fadeAnimation)
binding.playPauseView.visibility = View.GONE binding.playPauseView.visibility = View.GONE
} }
} }
fun doubleTapPlayPause() { fun doubleTapPlayPause() {
animationHandler.removeCallbacks(playPauseRunnable) animationHandler.removeCallbacks(doubleTapPlayPauseRunnable)
playPause(binding.playBtn) playPause(binding.playerControls.binding.playBtn)
if (!binding.controlsView.isVisible) { if (!binding.playerControls.binding.controlsView.isVisible) {
when { when {
player.paused!! -> { binding.playPauseView.setImageResource(R.drawable.ic_pause_80dp) } player.paused!! -> { binding.playPauseView.setImageResource(R.drawable.ic_pause_80dp) }
!player.paused!! -> { binding.playPauseView.setImageResource(R.drawable.ic_play_arrow_80dp) } !player.paused!! -> { binding.playPauseView.setImageResource(R.drawable.ic_play_arrow_80dp) }
} }
// if (binding.controlsView.isVisible) { binding.playPauseView.visibility = View.GONE; binding.playPauseView.setBackgroundColor(0x00000000) } else { binding.playPauseView.visibility = View.VISIBLE; binding.playPauseView.setBackgroundColor(0x70000000) }
AnimationUtils.loadAnimation(this, R.anim.fade_in_medium).also { fadeAnimation -> AnimationUtils.loadAnimation(this, R.anim.fade_in_medium).also { fadeAnimation ->
findViewById<ImageView>(R.id.playPauseView).startAnimation(fadeAnimation) binding.playPauseView.startAnimation(fadeAnimation)
binding.playPauseView.visibility = View.VISIBLE binding.playPauseView.visibility = View.VISIBLE
} }
animationHandler.postDelayed(playPauseRunnable, 500L) animationHandler.postDelayed(doubleTapPlayPauseRunnable, 500L)
} else binding.playPauseView.visibility = View.GONE } else binding.playPauseView.visibility = View.GONE
} }
@ -740,7 +505,7 @@ class PlayerActivity :
val newDiff = newPos - initialSeek val newDiff = newPos - initialSeek
// seek faster than assigning to timePos but less precise // seek faster than assigning to timePos but less precise
MPVLib.command(arrayOf("seek", newPos.toString(), "absolute+keyframes")) MPVLib.command(arrayOf("seek", newPos.toString(), "absolute+keyframes"))
updatePlaybackPos(newPos) binding.playerControls.updatePlaybackPos(newPos)
val diffText = Utils.prettyTime(newDiff, true) val diffText = Utils.prettyTime(newDiff, true)
binding.seekText.text = getString(R.string.ui_seek_distance, Utils.prettyTime(newPos), diffText) binding.seekText.text = getString(R.string.ui_seek_distance, Utils.prettyTime(newPos), diffText)
@ -828,13 +593,13 @@ class PlayerActivity :
fun switchDecoder(view: View) { fun switchDecoder(view: View) {
player.cycleHwdec() player.cycleHwdec()
preferences.getPlayerViewMode() preferences.getPlayerViewMode()
updateDecoderButton() binding.playerControls.updateDecoderButton()
} }
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun cycleSpeed(view: View) { fun cycleSpeed(view: View) {
player.cycleSpeed() player.cycleSpeed()
updateSpeedButton() binding.playerControls.updateSpeedButton()
} }
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
@ -846,16 +611,16 @@ class PlayerActivity :
// forces update of entire UI, used when resuming the activity // forces update of entire UI, used when resuming the activity
val paused = player.paused ?: return val paused = player.paused ?: return
updatePlaybackStatus(paused) updatePlaybackStatus(paused)
player.timePos?.let { updatePlaybackPos(it) } player.timePos?.let { binding.playerControls.updatePlaybackPos(it) }
player.duration?.let { updatePlaybackDuration(it) } player.duration?.let { binding.playerControls.updatePlaybackDuration(it) }
updatePlaylistButtons() updatePlaylistButtons()
updateEpisodeText() updateEpisodeText()
player.loadTracks() player.loadTracks()
} }
private fun updateEpisodeText() { private fun updateEpisodeText() {
binding.titleMainTxt.text = presenter.anime?.title binding.playerControls.binding.titleMainTxt.text = presenter.anime?.title
binding.titleSecondaryTxt.text = presenter.currentEpisode?.name binding.playerControls.binding.titleSecondaryTxt.text = presenter.currentEpisode?.name
} }
private fun updatePlaylistButtons() { private fun updatePlaylistButtons() {
@ -864,13 +629,13 @@ class PlayerActivity :
val g = ContextCompat.getColor(this, R.color.tint_disabled) val g = ContextCompat.getColor(this, R.color.tint_disabled)
val w = ContextCompat.getColor(this, R.color.tint_normal) val w = ContextCompat.getColor(this, R.color.tint_normal)
binding.prevBtn.imageTintList = ColorStateList.valueOf(if (plPos == 0) g else w) binding.playerControls.binding.prevBtn.imageTintList = ColorStateList.valueOf(if (plPos == 0) g else w)
binding.nextBtn.imageTintList = ColorStateList.valueOf(if (plPos == plCount - 1) g else w) binding.playerControls.binding.nextBtn.imageTintList = ColorStateList.valueOf(if (plPos == plCount - 1) g else w)
} }
private fun updatePlaybackStatus(paused: Boolean) { private fun updatePlaybackStatus(paused: Boolean) {
val r = if (paused) R.drawable.ic_play_arrow_80dp else R.drawable.ic_pause_80dp val r = if (paused) R.drawable.ic_play_arrow_80dp else R.drawable.ic_pause_80dp
binding.playBtn.setImageResource(r) binding.playerControls.binding.playBtn.setImageResource(r)
if (paused) { if (paused) {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
@ -912,14 +677,14 @@ class PlayerActivity :
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) { override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) {
isInPipMode = isInPictureInPictureMode isInPipMode = isInPictureInPictureMode
hideControls(!isInPictureInPictureMode) binding.playerControls.hideControls(!isInPictureInPictureMode)
if (isInPictureInPictureMode) binding.loadingIndicator.indicatorSize = binding.loadingIndicator.indicatorSize / 2 if (isInPictureInPictureMode) binding.loadingIndicator.indicatorSize = binding.loadingIndicator.indicatorSize / 2
else binding.loadingIndicator.indicatorSize = binding.loadingIndicator.indicatorSize * 2 else binding.loadingIndicator.indicatorSize = binding.loadingIndicator.indicatorSize * 2
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
if (isInPictureInPictureMode) { if (isInPictureInPictureMode) {
// On Android TV it is required to hide controller in this PIP change callback // On Android TV it is required to hide controller in this PIP change callback
hideControls(true) binding.playerControls.hideControls(true)
mReceiver = object : BroadcastReceiver() { mReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
if (intent == null || ACTION_MEDIA_CONTROL != intent.action) { if (intent == null || ACTION_MEDIA_CONTROL != intent.action) {
@ -951,14 +716,14 @@ class PlayerActivity :
unregisterReceiver(mReceiver) unregisterReceiver(mReceiver)
mReceiver = null mReceiver = null
} }
hideControls(false) binding.playerControls.hideControls(false)
} }
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private fun startPiP() { internal fun startPiP() {
if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
hideControls(true) binding.playerControls.hideControls(true)
player.paused?.let { updatePictureInPictureActions(!it) } player.paused?.let { updatePictureInPictureActions(!it) }
?.let { this.enterPictureInPictureMode(it) } ?.let { this.enterPictureInPictureMode(it) }
} }
@ -1191,8 +956,8 @@ class PlayerActivity :
private fun eventPropertyUi(property: String, value: Long) { private fun eventPropertyUi(property: String, value: Long) {
when (property) { when (property) {
"time-pos" -> updatePlaybackPos(value.toInt()) "time-pos" -> binding.playerControls.updatePlaybackPos(value.toInt())
"duration" -> updatePlaybackDuration(value.toInt()) "duration" -> binding.playerControls.updatePlaybackDuration(value.toInt())
} }
} }
@ -1224,3 +989,14 @@ class PlayerActivity :
} }
} }
} }
private const val ACTION_MEDIA_CONTROL = "media_control"
private const val EXTRA_CONTROL_TYPE = "control_type"
private const val REQUEST_PLAY = 1
private const val REQUEST_PAUSE = 2
private const val CONTROL_TYPE_PLAY = 1
private const val CONTROL_TYPE_PAUSE = 2
private const val REQUEST_PREVIOUS = 3
private const val REQUEST_NEXT = 4
private const val CONTROL_TYPE_PREVIOUS = 3
private const val CONTROL_TYPE_NEXT = 4

View file

@ -0,0 +1,274 @@
package eu.kanade.tachiyomi.ui.player
import android.content.Context
import android.content.ContextWrapper
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.animation.AnimationUtils
import android.widget.LinearLayout
import android.widget.SeekBar
import androidx.annotation.StringRes
import androidx.core.view.isVisible
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.PlayerControlsBinding
import `is`.xyz.mpv.MPVLib
import `is`.xyz.mpv.PickerDialog
import `is`.xyz.mpv.SpeedPickerDialog
import `is`.xyz.mpv.StateRestoreCallback
import `is`.xyz.mpv.Utils
class PlayerControlsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
LinearLayout(context, attrs) {
internal val binding: PlayerControlsBinding =
PlayerControlsBinding.inflate(LayoutInflater.from(context), this, false)
val activity: PlayerActivity = context.getActivity()!!
private var userIsOperatingSeekbar = false
private val seekBarChangeListener = object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (!fromUser) {
return
}
activity.player.timePos = progress
updatePlaybackPos(progress)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {
userIsOperatingSeekbar = true
}
override fun onStopTrackingTouch(seekBar: SeekBar) {
userIsOperatingSeekbar = false
}
}
private tailrec fun Context.getActivity(): PlayerActivity? = this as? PlayerActivity
?: (this as? ContextWrapper)?.baseContext?.getActivity()
init {
addView(binding.root)
}
override fun onViewAdded(child: View?) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
binding.pipBtn.isVisible = context.packageManager.hasSystemFeature(
PackageManager.FEATURE_PICTURE_IN_PICTURE,
) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
}
binding.backArrowBtn.setOnClickListener { activity.finishAndRemoveTask() }
binding.pipBtn.setOnClickListener { activity.startPiP() }
// Lock and Unlock controls
binding.lockBtn.setOnClickListener { activity.isLocked = true; toggleControls() }
binding.unlockBtn.setOnClickListener { activity.isLocked = false; toggleControls() }
// Cycle, Long click controls
binding.cycleAudioBtn.setOnLongClickListener { pickAudio(); true }
binding.cycleSpeedBtn.setOnLongClickListener { pickSpeed(); true }
binding.cycleSubsBtn.setOnLongClickListener { pickSub(); true }
binding.playbackSeekbar.setOnSeekBarChangeListener(seekBarChangeListener)
binding.nextBtn.setOnClickListener { activity.switchEpisode(false) }
binding.prevBtn.setOnClickListener { activity.switchEpisode(true) }
}
private val animationHandler = Handler(Looper.getMainLooper())
// Fade out Player controls
private val controlsViewRunnable = Runnable {
if (activity.isLocked) {
fadeOutView(binding.lockedView)
} else {
fadeOutView(binding.controlsView)
}
}
internal fun toggleControls() {
if (activity.isLocked) {
// Hide controls
binding.controlsView.isVisible = false
if (!binding.lockedView.isVisible && !activity.player.paused!!) {
showAndFadeControls()
} else if (!binding.lockedView.isVisible && activity.player.paused!!) {
fadeInView(binding.lockedView)
} else {
fadeOutView(binding.lockedView)
}
} else {
if (!binding.controlsView.isVisible && !activity.player.paused!!) {
showAndFadeControls()
} else if (!binding.controlsView.isVisible && activity.player.paused!!) {
fadeInView(binding.controlsView)
} else {
fadeOutView(binding.controlsView)
}
binding.lockedView.isVisible = false
}
}
internal fun hideControls(hide: Boolean) {
binding.controlsView.isVisible = !hide
}
internal fun updatePlaybackPos(position: Int) {
binding.playbackPositionTxt.text = Utils.prettyTime(position)
if (!userIsOperatingSeekbar) {
binding.playbackSeekbar.progress = position
}
updateDecoderButton()
updateSpeedButton()
}
internal fun updatePlaybackDuration(duration: Int) {
binding.playbackDurationTxt.text = Utils.prettyTime(duration)
if (!userIsOperatingSeekbar) {
binding.playbackSeekbar.max = duration
}
}
internal fun updateDecoderButton() {
if (binding.cycleDecoderBtn.visibility != View.VISIBLE) {
return
}
binding.cycleDecoderBtn.text = if (activity.player.hwdecActive) "HW" else "SW"
}
internal fun updateSpeedButton() {
binding.cycleSpeedBtn.text = context.getString(R.string.ui_speed, activity.player.playbackSpeed)
}
internal fun showAndFadeControls() {
val itemView = if (!activity.isLocked) binding.controlsView else binding.lockedView
animationHandler.removeCallbacks(controlsViewRunnable)
itemView.visibility = View.VISIBLE
animationHandler.postDelayed(controlsViewRunnable, 3500L)
}
private fun fadeOutView(view: View) {
animationHandler.removeCallbacks(controlsViewRunnable)
AnimationUtils.loadAnimation(context, R.anim.fade_out_medium).also { fadeAnimation ->
view.startAnimation(fadeAnimation)
view.visibility = View.GONE
}
}
private fun fadeInView(view: View) {
animationHandler.removeCallbacks(controlsViewRunnable)
AnimationUtils.loadAnimation(context, R.anim.fade_in_short).also { fadeAnimation ->
view.startAnimation(fadeAnimation)
view.visibility = View.VISIBLE
}
}
private fun pauseForDialog(): StateRestoreCallback {
val wasPlayerPaused = activity.player.paused ?: true // default to not changing state
activity.player.paused = true
return {
if (!wasPlayerPaused) {
activity.player.paused = false
}
}
}
internal fun playPause() {
when {
activity.player.paused!! -> animationHandler.removeCallbacks(controlsViewRunnable)
binding.controlsView.isVisible -> {
showAndFadeControls()
}
}
}
private fun pickAudio() {
val restore = pauseForDialog()
with(MaterialAlertDialogBuilder(context)) {
setSingleChoiceItems(
activity.audioTracks.map { it.lang }.toTypedArray(),
activity.selectedAudio,
) { dialog, item ->
if (item == activity.selectedAudio) return@setSingleChoiceItems
if (item == 0) {
activity.selectedAudio = 0
activity.player.aid = -1
return@setSingleChoiceItems
}
activity.setAudio(item)
dialog.dismiss()
}
setOnDismissListener { restore() }
create().show()
}
}
private fun pickSub() {
val restore = pauseForDialog()
with(MaterialAlertDialogBuilder(context)) {
setSingleChoiceItems(
activity.subTracks.map { it.lang }.toTypedArray(),
activity.selectedSub,
) { dialog, item ->
if (item == 0) {
activity.selectedSub = 0
activity.player.sid = -1
return@setSingleChoiceItems
}
activity.setSub(item)
dialog.dismiss()
}
setOnDismissListener { restore() }
create().show()
}
}
private fun pickSpeed() {
// TODO: replace this with SliderPickerDialog
val picker = SpeedPickerDialog()
val restore = pauseForDialog()
speedPickerDialog(picker, R.string.title_speed_dialog) {
updateSpeedButton()
restore()
}
}
private fun speedPickerDialog(
picker: PickerDialog,
@StringRes titleRes: Int,
restoreState: StateRestoreCallback,
) {
val dialog = with(MaterialAlertDialogBuilder(context)) {
setTitle(titleRes)
setView(picker.buildView(LayoutInflater.from(context)))
setPositiveButton(R.string.dialog_ok) { _, _ ->
picker.number?.let {
if (picker.isInteger()) {
MPVLib.setPropertyInt("speed", it.toInt())
} else {
MPVLib.setPropertyDouble("speed", it)
}
}
}
setNegativeButton(R.string.dialog_cancel) { dialog, _ -> dialog.cancel() }
setOnDismissListener { restoreState() }
create()
}
picker.number = MPVLib.getPropertyDouble("speed")
dialog.show()
}
}

View file

@ -48,334 +48,10 @@
app:tint="?attr/colorAccent" /> app:tint="?attr/colorAccent" />
</LinearLayout> </LinearLayout>
<LinearLayout <eu.kanade.tachiyomi.ui.player.PlayerControlsView
android:id="@+id/lockedView" android:id="@+id/player_controls"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"/>
android:layout_alignParentLeft="true"
android:layout_marginTop="10dp"
android:layout_marginLeft="10dp"
android:visibility="gone">
<ImageButton
android:id="@+id/unlockBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Unlock player"
android:src="@drawable/ic_lock_open_24dp"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="?attr/colorOnPrimarySurface" />
</LinearLayout>
<!-- Double layout for consistency in code -->
<LinearLayout
android:id="@+id/controlsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#70000000">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:ignore="UselessParent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/controls_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<ImageButton
android:id="@+id/backArrowBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Go back"
android:src="@drawable/ic_arrow_back_24dp"
android:layout_marginHorizontal="10dp"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="?attr/colorOnPrimarySurface"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/fullTitleTxt"
app:layout_constraintTop_toTopOf="parent"/>
<LinearLayout
android:id="@+id/fullTitleTxt"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintLeft_toRightOf="@id/backArrowBtn"
app:layout_constraintRight_toLeftOf="@id/cycleDecoderBtn"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/titleMainTxt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/colorOnPrimarySurface"
android:text=""
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/titleSecondaryTxt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/colorOnPrimarySurface"
android:alpha = "0.5"
android:textSize="12sp"
android:text="" />
</LinearLayout>
<ImageButton
android:id="@+id/settingsBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Settings"
android:background="?attr/selectableItemBackground"
android:onClick="openSettings"
android:layout_marginRight="10dp"
android:src="@drawable/ic_settings_24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:tint="?attr/colorOnPrimarySurface" />
<ImageButton
android:id="@+id/cycleSubsBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Subtitles"
android:onClick="cycleSub"
android:src="@drawable/ic_subtitles_black_24dp"
android:background="?attr/selectableItemBackground"
app:tint="?attr/colorOnPrimarySurface"
app:layout_constraintRight_toRightOf="@id/cycleAudioBtn"
app:layout_constraintRight_toLeftOf="@id/settingsBtn"
app:layout_constraintTop_toTopOf="parent"/>
<ImageButton
android:id="@+id/cycleAudioBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Audio"
android:background="?attr/selectableItemBackground"
android:onClick="cycleAudio"
android:src="@drawable/ic_audiotrack_black_24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="@id/cycleDecoderBtn"
app:layout_constraintRight_toLeftOf="@id/cycleSubsBtn"
app:tint="?attr/colorOnPrimarySurface" />
<Button
android:id="@+id/cycleDecoderBtn"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_toLeftOf="@id/cycleAudioBtn"
android:background="?attr/selectableItemBackground"
android:onClick="switchDecoder"
android:text=".."
android:textColor="?attr/colorOnPrimarySurface"
app:layout_constraintRight_toLeftOf="@id/cycleAudioBtn"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<RelativeLayout
android:id="@+id/controls_bottom"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:visibility="visible">
<ImageButton
android:id="@+id/play_btn"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_centerInParent="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Play/Pause"
android:onClick="playPause"
android:textColor="@android:color/white"
android:visibility="gone"
app:tint="?attr/colorOnPrimarySurface"
tools:src="@drawable/ic_play_arrow_80dp"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/control_bar"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:orientation="vertical"
tools:visibility="visible">
<LinearLayout
android:id="@+id/controls_title_group"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="visible">
<!-- These two are only used for audio -->
<TextView
android:id="@+id/titleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="-"
android:textColor="@color/tint_normal"
android:textSize="24sp"
android:visibility="gone" />
<TextView
android:id="@+id/minorTitleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="-"
android:textColor="@color/tint_normal"
android:textSize="14sp"
android:visibility="gone" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageButton
android:id="@+id/lockBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginLeft="10dp"
android:contentDescription="Lock player"
android:background="?attr/selectableItemBackground"
android:src="@drawable/ic_lock_24dp"
app:tint="?attr/colorOnPrimarySurface" />
<Button
android:id="@+id/cycleSpeedBtn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground"
android:onClick="cycleSpeed"
android:text=".."
android:textColor="?attr/colorOnPrimarySurface" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:gravity="right"
android:orientation="horizontal">
<Button
android:id="@+id/controls_skip_intro_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground"
android:onClick="skipIntro"
android:text="@string/player_controls_skip_intro_text"
android:textColor="?attr/colorOnPrimarySurface" />
<ImageButton
android:id="@+id/cycleViewModeBtn"
android:layout_width="48dp"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground"
android:contentDescription="Cycle view modes"
android:onClick="cycleViewMode"
android:src="@drawable/ic_fullscreen_black_24dp"
app:tint="?attr/colorOnPrimarySurface" />
<ImageButton
android:id="@+id/pipBtn"
android:layout_width="48dp"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/action_player_pip"
android:src="@drawable/ic_picture_in_picture_24dp"
android:visibility="visible" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/controls_seekbar_group"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:weightSum="100">
<ImageButton
android:id="@+id/prevBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginLeft="10dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_previous_episode"
android:padding="@dimen/screen_edge_margin"
app:srcCompat="@drawable/ic_skip_previous_24dp"
app:tint="?attr/colorOnPrimarySurface" />
<TextView
android:id="@+id/playbackPositionTxt"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="10"
android:gravity="center"
android:text="0:00"
android:textColor="@android:color/white" />
<SeekBar
android:id="@+id/playbackSeekbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="80"
android:progressBackgroundTint="@color/tint_seekbar_bg" />
<TextView
android:id="@+id/playbackDurationTxt"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="10"
android:gravity="center"
android:text="0:00"
android:textColor="@android:color/white" />
<ImageButton
android:id="@+id/nextBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginRight="10dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_next_episode"
android:padding="@dimen/screen_edge_margin"
app:srcCompat="@drawable/ic_skip_next_24dp"
app:tint="?attr/colorOnPrimarySurface" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/volumeView" android:id="@+id/volumeView"
@ -491,7 +167,7 @@
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:contentDescription="Play/Pause" android:contentDescription="Play/Pause"
android:textColor="@android:color/white" android:textColor="@android:color/white"
android:background="#70000000" android:background="#00000000"
android:visibility="gone" android:visibility="gone"
app:tint="?attr/colorOnPrimarySurface" app:tint="?attr/colorOnPrimarySurface"
tools:src="@drawable/ic_play_arrow_80dp" /> tools:src="@drawable/ic_play_arrow_80dp" />

View file

@ -0,0 +1,339 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="RtlHardcoded,HardcodedText" >
<LinearLayout
android:id="@+id/lockedView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentLeft="true"
android:layout_marginTop="10dp"
android:layout_marginLeft="10dp"
android:visibility="gone">
<ImageButton
android:id="@+id/unlockBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Unlock player"
android:src="@drawable/ic_lock_open_24dp"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="?attr/colorOnPrimarySurface" />
</LinearLayout>
<!-- Double layout for consistency in code -->
<LinearLayout
android:id="@+id/controlsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#70000000">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:ignore="UselessParent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/controls_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp">
<ImageButton
android:id="@+id/backArrowBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Go back"
android:src="@drawable/ic_arrow_back_24dp"
android:layout_marginHorizontal="10dp"
android:background="?attr/selectableItemBackgroundBorderless"
app:tint="?attr/colorOnPrimarySurface"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/fullTitleTxt"
app:layout_constraintTop_toTopOf="parent"/>
<LinearLayout
android:id="@+id/fullTitleTxt"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintLeft_toRightOf="@id/backArrowBtn"
app:layout_constraintRight_toLeftOf="@id/cycleDecoderBtn"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/titleMainTxt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/colorOnPrimarySurface"
android:text=""
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/titleSecondaryTxt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?attr/colorOnPrimarySurface"
android:alpha = "0.5"
android:textSize="12sp"
android:text="" />
</LinearLayout>
<ImageButton
android:id="@+id/settingsBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Settings"
android:background="?attr/selectableItemBackground"
android:onClick="openSettings"
android:layout_marginRight="10dp"
android:src="@drawable/ic_settings_24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:tint="?attr/colorOnPrimarySurface" />
<ImageButton
android:id="@+id/cycleSubsBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Subtitles"
android:onClick="cycleSub"
android:src="@drawable/ic_subtitles_black_24dp"
android:background="?attr/selectableItemBackground"
app:tint="?attr/colorOnPrimarySurface"
app:layout_constraintRight_toRightOf="@id/cycleAudioBtn"
app:layout_constraintRight_toLeftOf="@id/settingsBtn"
app:layout_constraintTop_toTopOf="parent"/>
<ImageButton
android:id="@+id/cycleAudioBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:contentDescription="Audio"
android:background="?attr/selectableItemBackground"
android:onClick="cycleAudio"
android:src="@drawable/ic_audiotrack_black_24dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="@id/cycleDecoderBtn"
app:layout_constraintRight_toLeftOf="@id/cycleSubsBtn"
app:tint="?attr/colorOnPrimarySurface" />
<Button
android:id="@+id/cycleDecoderBtn"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_toLeftOf="@id/cycleAudioBtn"
android:background="?attr/selectableItemBackground"
android:onClick="switchDecoder"
android:text=".."
android:textColor="?attr/colorOnPrimarySurface"
app:layout_constraintRight_toLeftOf="@id/cycleAudioBtn"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<RelativeLayout
android:id="@+id/controls_bottom"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:visibility="visible">
<ImageButton
android:id="@+id/play_btn"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_centerInParent="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Play/Pause"
android:onClick="playPause"
android:textColor="@android:color/white"
android:visibility="gone"
app:tint="?attr/colorOnPrimarySurface"
tools:src="@drawable/ic_play_arrow_80dp"
tools:visibility="visible" />
<LinearLayout
android:id="@+id/control_bar"
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:orientation="vertical"
tools:visibility="visible">
<LinearLayout
android:id="@+id/controls_title_group"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="visible">
<!-- These two are only used for audio -->
<TextView
android:id="@+id/titleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="-"
android:textColor="@color/tint_normal"
android:textSize="24sp"
android:visibility="gone" />
<TextView
android:id="@+id/minorTitleTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="-"
android:textColor="@color/tint_normal"
android:textSize="14sp"
android:visibility="gone" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageButton
android:id="@+id/lockBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginLeft="10dp"
android:contentDescription="Lock player"
android:background="?attr/selectableItemBackground"
android:src="@drawable/ic_lock_24dp"
app:tint="?attr/colorOnPrimarySurface" />
<Button
android:id="@+id/cycleSpeedBtn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground"
android:onClick="cycleSpeed"
android:text=".."
android:textColor="?attr/colorOnPrimarySurface" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:gravity="right"
android:orientation="horizontal">
<Button
android:id="@+id/controls_skip_intro_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground"
android:onClick="skipIntro"
android:text="@string/player_controls_skip_intro_text"
android:textColor="?attr/colorOnPrimarySurface" />
<ImageButton
android:id="@+id/cycleViewModeBtn"
android:layout_width="48dp"
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground"
android:contentDescription="Cycle view modes"
android:onClick="cycleViewMode"
android:src="@drawable/ic_fullscreen_black_24dp"
app:tint="?attr/colorOnPrimarySurface" />
<ImageButton
android:id="@+id/pipBtn"
android:layout_width="48dp"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/action_player_pip"
android:src="@drawable/ic_picture_in_picture_24dp"
android:visibility="visible" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/controls_seekbar_group"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:weightSum="100">
<ImageButton
android:id="@+id/prevBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginLeft="10dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_previous_episode"
android:padding="@dimen/screen_edge_margin"
app:srcCompat="@drawable/ic_skip_previous_24dp"
app:tint="?attr/colorOnPrimarySurface" />
<TextView
android:id="@+id/playbackPositionTxt"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="10"
android:gravity="center"
android:text="0:00"
android:textColor="@android:color/white" />
<SeekBar
android:id="@+id/playbackSeekbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="80"
android:progressBackgroundTint="@color/tint_seekbar_bg" />
<TextView
android:id="@+id/playbackDurationTxt"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_weight="10"
android:gravity="center"
android:text="0:00"
android:textColor="@android:color/white" />
<ImageButton
android:id="@+id/nextBtn"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginRight="10dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_next_episode"
android:padding="@dimen/screen_edge_margin"
app:srcCompat="@drawable/ic_skip_next_24dp"
app:tint="?attr/colorOnPrimarySurface" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
</LinearLayout>
</RelativeLayout>