feat: lock player controls (#372)

* feat: lock player controls

Add Lock Player feature.

This feature is similar to the feature found in the stock Jellyfin Android app and in the VLC app.

When enabled, it disables the gestures (seek, volume and brightness control), disables the default player controls and leaves only two buttons: a back button and an "unlock" button.
Pressing the unlock button reverts the player back to the initial state, e.g. gestures enabled and default buttons shown.

Works with ExoPlayer and MPV Player.

Let me know of any issues.

* Update PlayerGestureHelper.kt

* Fixed spacing issues

* fixed bug + formatting

* Simplified code

* Fixed spacing

* fixed trailing line?

* refactor: set locked layout initial visibility in xml

---------

Co-authored-by: jarnedemeulemeester <jarnedemeulemeester@gmail.com>
This commit is contained in:
004matteos 2023-05-25 10:20:30 +03:00 committed by GitHub
parent 12b819bc7a
commit a886baf907
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 399 additions and 226 deletions

View file

@ -1,11 +1,13 @@
package dev.jdtech.jellyfin
import android.content.Context
import android.content.pm.ActivityInfo
import android.media.AudioManager
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import android.widget.Button
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.TextView
@ -29,6 +31,8 @@ import dev.jdtech.jellyfin.viewmodels.PlayerActivityViewModel
import javax.inject.Inject
import timber.log.Timber
var isControlsLocked: Boolean = false
@AndroidEntryPoint
class PlayerActivity : BasePlayerActivity() {
@ -67,6 +71,11 @@ class PlayerActivity : BasePlayerActivity() {
finish()
}
binding.playerView.findViewById<View>(R.id.back_button_alt).setOnClickListener {
finish()
isControlsLocked = false
}
val videoNameTextView = binding.playerView.findViewById<TextView>(R.id.video_name)
viewModel.currentItemTitle.observe(this) { title ->
@ -77,10 +86,15 @@ class PlayerActivity : BasePlayerActivity() {
val subtitleButton = binding.playerView.findViewById<ImageButton>(R.id.btn_subtitle)
val speedButton = binding.playerView.findViewById<ImageButton>(R.id.btn_speed)
val skipIntroButton = binding.playerView.findViewById<Button>(R.id.btn_skip_intro)
val lockButton = binding.playerView.findViewById<ImageButton>(R.id.btn_lockview)
val unlockButton = binding.playerView.findViewById<ImageButton>(R.id.btn_unlock)
audioButton.isEnabled = false
audioButton.imageAlpha = 75
lockButton.isEnabled = false
lockButton.imageAlpha = 75
subtitleButton.isEnabled = false
subtitleButton.imageAlpha = 75
@ -120,6 +134,23 @@ class PlayerActivity : BasePlayerActivity() {
}
}
val exoPlayerControlView = findViewById<FrameLayout>(R.id.player_controls)
val lockedLayout = findViewById<FrameLayout>(R.id.locked_player_view)
lockButton.setOnClickListener {
exoPlayerControlView.visibility = View.GONE
lockedLayout.visibility = View.VISIBLE
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
isControlsLocked = true
}
unlockButton.setOnClickListener {
exoPlayerControlView.visibility = View.VISIBLE
lockedLayout.visibility = View.GONE
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
isControlsLocked = false
}
subtitleButton.setOnClickListener {
when (viewModel.player) {
is MPVPlayer -> {
@ -189,6 +220,8 @@ class PlayerActivity : BasePlayerActivity() {
if (it) {
audioButton.isEnabled = true
audioButton.imageAlpha = 255
lockButton.isEnabled = true
lockButton.imageAlpha = 255
subtitleButton.isEnabled = true
subtitleButton.imageAlpha = 255
speedButton.isEnabled = true

View file

@ -18,6 +18,7 @@ import androidx.media3.ui.PlayerView
import dev.jdtech.jellyfin.AppPreferences
import dev.jdtech.jellyfin.Constants
import dev.jdtech.jellyfin.PlayerActivity
import dev.jdtech.jellyfin.isControlsLocked
import dev.jdtech.jellyfin.mpv.MPVPlayer
import kotlin.math.abs
import timber.log.Timber
@ -65,6 +66,8 @@ class PlayerGestureHelper(
}
override fun onDoubleTap(e: MotionEvent): Boolean {
// Disables double tap gestures if view is locked
if (isControlsLocked) return false
val viewCenterX = playerView.measuredWidth / 2
val currentPos = playerView.player?.currentPosition ?: 0
@ -90,6 +93,8 @@ class PlayerGestureHelper(
): Boolean {
// Excludes area where app gestures conflicting with system gestures
if (inExclusionArea(firstEvent)) return false
// Disables seek gestures if view is locked
if (isControlsLocked) return false
// Check whether swipe was oriented vertically
if (abs(distanceY / distanceX) < 2) {
@ -128,6 +133,8 @@ class PlayerGestureHelper(
): Boolean {
// Excludes area where app gestures conflicting with system gestures
if (inExclusionArea(firstEvent)) return false
// Disables volume gestures when player is locked
if (isControlsLocked) return false
if (abs(distanceY / distanceX) < 2) return false
@ -213,6 +220,8 @@ class PlayerGestureHelper(
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean = true
override fun onScale(detector: ScaleGestureDetector): Boolean {
// Disables zoom gesture if view is locked
if (isControlsLocked) return false
lastScaleEvent = SystemClock.elapsedRealtime()
val scaleFactor = detector.scaleFactor
if (abs(scaleFactor - Constants.ZOOM_SCALE_BASE) > Constants.ZOOM_SCALE_THRESHOLD) {

View file

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:id="@+id/locked_player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:visibility="visible">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="38dp"
android:orientation="horizontal"
app:layout_constraintEnd_toStartOf="@id/extra_buttons_alt"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/back_button_alt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/rounded_corner"
android:contentDescription="@string/player_controls_exit"
android:padding="16dp"
android:src="@drawable/ic_arrow_left" />
<Space
android:layout_width="16dp"
android:layout_height="0dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/extra_buttons_alt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/btn_unlock"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="@drawable/rounded_corner"
android:contentDescription="@string/select_playback_speed"
android:padding="16dp"
android:src="@drawable/ic_unlock"
app:tint="@android:color/white" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View file

@ -0,0 +1,249 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:id="@+id/player_controls"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/player_background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toStartOf="@id/extra_buttons"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/transparent_circle_background"
android:contentDescription="@string/player_controls_exit"
android:padding="16dp"
android:src="@drawable/ic_arrow_left" />
<Space
android:layout_width="16dp"
android:layout_height="0dp" />
<TextView
android:id="@+id/video_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:textColor="@android:color/white"
tools:text="The Dawn of Despair" />
</LinearLayout>
<LinearLayout
android:id="@+id/extra_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/btn_lockview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_weight="1"
android:background="@drawable/transparent_circle_background"
android:contentDescription="Locks the player"
android:padding="16dp"
android:src="@drawable/ic_lock"
app:tint="@android:color/white" />
<Space
android:layout_width="16dp"
android:layout_height="0dp"
android:layout_weight="1" />
<ImageButton
android:id="@+id/btn_speed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="@drawable/transparent_circle_background"
android:contentDescription="@string/select_playback_speed"
android:padding="16dp"
android:src="@drawable/ic_gauge"
app:tint="@android:color/white" />
<Space
android:layout_width="16dp"
android:layout_height="0dp" />
<ImageButton
android:id="@+id/btn_audio_track"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="@drawable/transparent_circle_background"
android:contentDescription="@string/select_audio_track"
android:padding="16dp"
android:src="@drawable/ic_speaker"
app:tint="@android:color/white" />
<Space
android:layout_width="16dp"
android:layout_height="0dp" />
<ImageButton
android:id="@+id/btn_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="@drawable/transparent_circle_background"
android:contentDescription="@string/select_subtile_track"
android:padding="16dp"
android:src="@drawable/ic_closed_caption"
app:tint="@android:color/white" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageButton
android:id="@+id/exo_prev"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:background="@drawable/transparent_circle_background"
android:padding="16dp"
android:src="@drawable/ic_skip_back" />
<ImageButton
android:id="@+id/exo_rew"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:background="@drawable/transparent_circle_background"
android:padding="16dp"
android:src="@drawable/ic_rewind" />
<ImageButton
android:id="@+id/exo_play_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/circle_background"
android:backgroundTint="@android:color/white"
android:padding="16dp"
android:src="@drawable/ic_play"
app:tint="@android:color/black" />
<ImageButton
android:id="@+id/exo_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/circle_background"
android:backgroundTint="@android:color/white"
android:padding="16dp"
android:src="@drawable/ic_pause"
android:visibility="gone"
app:tint="@android:color/black" />
<ImageButton
android:id="@+id/exo_ffwd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:background="@drawable/transparent_circle_background"
android:padding="16dp"
android:src="@drawable/ic_fast_forward" />
<ImageButton
android:id="@+id/exo_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:background="@drawable/transparent_circle_background"
android:padding="16dp"
android:src="@drawable/ic_skip_forward" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical"
android:padding="16dp">
<ImageView
android:id="@+id/image_preview"
android:layout_width="160dp"
android:layout_height="90dp"
android:layout_gravity="start"
android:background="@android:color/transparent"
android:visibility="gone"
tools:visibility="visible" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<TextView
android:id="@+id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="14sp"
tools:text="00:00" />
<Space
android:layout_width="8dp"
android:layout_height="0dp" />
<View
android:layout_width="4dp"
android:layout_height="1dp"
android:background="@android:color/white" />
<Space
android:layout_width="8dp"
android:layout_height="0dp" />
<TextView
android:id="@+id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="14sp"
tools:text="24:21" />
</LinearLayout>
<androidx.media3.ui.DefaultTimeBar
android:id="@+id/exo_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:played_color="?attr/colorPrimary" />
</LinearLayout>
</FrameLayout>

View file

@ -1,232 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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:id="@+id/player_controls"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/exo_main_controls"/>
<include
layout="@layout/exo_locked_controls"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/player_background">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toStartOf="@id/extra_buttons"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/back_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/transparent_circle_background"
android:contentDescription="@string/player_controls_exit"
android:padding="16dp"
android:src="@drawable/ic_arrow_left" />
<Space
android:layout_width="16dp"
android:layout_height="0dp" />
<TextView
android:id="@+id/video_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Material3.TitleLarge"
android:textColor="@android:color/white"
tools:text="The Dawn of Despair" />
</LinearLayout>
<LinearLayout
android:id="@+id/extra_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageButton
android:id="@+id/btn_speed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="@drawable/transparent_circle_background"
android:contentDescription="@string/select_playback_speed"
android:padding="16dp"
android:src="@drawable/ic_gauge"
app:tint="@android:color/white" />
<Space
android:layout_width="16dp"
android:layout_height="0dp" />
<ImageButton
android:id="@+id/btn_audio_track"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="@drawable/transparent_circle_background"
android:contentDescription="@string/select_audio_track"
android:padding="16dp"
android:src="@drawable/ic_speaker"
app:tint="@android:color/white" />
<Space
android:layout_width="16dp"
android:layout_height="0dp" />
<ImageButton
android:id="@+id/btn_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:background="@drawable/transparent_circle_background"
android:contentDescription="@string/select_subtile_track"
android:padding="16dp"
android:src="@drawable/ic_closed_caption"
app:tint="@android:color/white" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageButton
android:id="@+id/exo_prev"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:background="@drawable/transparent_circle_background"
android:padding="16dp"
android:src="@drawable/ic_skip_back" />
<ImageButton
android:id="@+id/exo_rew"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
android:background="@drawable/transparent_circle_background"
android:padding="16dp"
android:src="@drawable/ic_rewind" />
<ImageButton
android:id="@+id/exo_play_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/circle_background"
android:backgroundTint="@android:color/white"
android:padding="16dp"
android:src="@drawable/ic_play"
app:tint="@android:color/black" />
<ImageButton
android:id="@+id/exo_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/circle_background"
android:backgroundTint="@android:color/white"
android:padding="16dp"
android:src="@drawable/ic_pause"
android:visibility="gone"
app:tint="@android:color/black" />
<ImageButton
android:id="@+id/exo_ffwd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:background="@drawable/transparent_circle_background"
android:padding="16dp"
android:src="@drawable/ic_fast_forward" />
<ImageButton
android:id="@+id/exo_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:background="@drawable/transparent_circle_background"
android:padding="16dp"
android:src="@drawable/ic_skip_forward" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical"
android:padding="16dp">
<ImageView
android:id="@+id/image_preview"
android:layout_width="160dp"
android:layout_height="90dp"
android:layout_gravity="start"
android:background="@android:color/transparent"
android:visibility="gone"
tools:visibility="visible" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<TextView
android:id="@+id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="14sp"
tools:text="00:00" />
<Space
android:layout_width="8dp"
android:layout_height="0dp" />
<View
android:layout_width="4dp"
android:layout_height="1dp"
android:background="@android:color/white" />
<Space
android:layout_width="8dp"
android:layout_height="0dp" />
<TextView
android:id="@+id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="14sp"
tools:text="24:21" />
</LinearLayout>
<androidx.media3.ui.DefaultTimeBar
android:id="@+id/exo_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:played_color="?attr/colorPrimary" />
</LinearLayout>
android:layout_gravity="bottom|end" />
</FrameLayout>

View file

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M5,11L19,11A2,2 0,0 1,21 13L21,20A2,2 0,0 1,19 22L5,22A2,2 0,0 1,3 20L3,13A2,2 0,0 1,5 11z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
<path
android:pathData="M7,11V7a5,5 0,0 1,9.9 -1"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="@android:color/white"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<item android:id="@android:id/mask">
<shape android:shape="oval">
<solid android:color="@android:color/white" />
</shape>
</item>
<item>
<shape>
<solid android:color="#72000000" />
<corners android:radius="10000dp" />
</shape>
</item>
</ripple>